From 28f8e039ccd5dfcb31e44524d340782a7496c938 Mon Sep 17 00:00:00 2001 From: gregorni <119804311+gregorni@users.noreply.github.com> Date: Fri, 24 Feb 2023 17:16:19 +0100 Subject: [PATCH] Add first draft of GPU acceleration --- build-aux/python3-glcontext.json | 14 ++ build-aux/python3-moderngl.json | 14 ++ data/io.github.fsobolev.Cavalier.gschema.xml | 5 + io.github.fsobolev.Cavalier.json | 4 +- src/gl_area.py | 155 +++++++++++++++++++ src/preferences_window.py | 16 ++ src/window.py | 3 + 7 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 build-aux/python3-glcontext.json create mode 100644 build-aux/python3-moderngl.json create mode 100644 src/gl_area.py diff --git a/build-aux/python3-glcontext.json b/build-aux/python3-glcontext.json new file mode 100644 index 0000000..3339958 --- /dev/null +++ b/build-aux/python3-glcontext.json @@ -0,0 +1,14 @@ +{ + "name": "python3-glcontext", + "buildsystem": "simple", + "build-commands": [ + "pip3 install --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"glcontext==2.3.7\" --no-build-isolation" + ], + "sources": [ + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/24/41/181a4354bf6373bc8a704f30a8fc059eed87bea7d77a1eb79fb57c8772b9/glcontext-2.3.7.tar.gz", + "sha256": "bb2d0503f45ad85ca7319bd37fd983e374b3f824c38a450b5f72cfc974114156" + } + ] +} \ No newline at end of file diff --git a/build-aux/python3-moderngl.json b/build-aux/python3-moderngl.json new file mode 100644 index 0000000..c34cccc --- /dev/null +++ b/build-aux/python3-moderngl.json @@ -0,0 +1,14 @@ +{ + "name": "python3-moderngl", + "buildsystem": "simple", + "build-commands": [ + "pip3 install --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"moderngl==5.7.4\" --no-build-isolation" + ], + "sources": [ + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/2b/fa/a04279fad74b9e3c36f30963f9a8b1586930d246728b858cf5cc90bdd699/moderngl-5.7.4.tar.gz", + "sha256": "20f821bf66b2811bc8648d7cf7f64402afff7619fea271f42a6ee85fe03e4041" + } + ] +} \ No newline at end of file diff --git a/data/io.github.fsobolev.Cavalier.gschema.xml b/data/io.github.fsobolev.Cavalier.gschema.xml index 78985fe..5277acc 100644 --- a/data/io.github.fsobolev.Cavalier.gschema.xml +++ b/data/io.github.fsobolev.Cavalier.gschema.xml @@ -20,6 +20,11 @@ Whether to hide headerbar when main window is not focused. false + + GPU acceleration (experimental) + Whether to render the visualization using the GPU. + false + Drawing mode Defines what the visualizer looks like. diff --git a/io.github.fsobolev.Cavalier.json b/io.github.fsobolev.Cavalier.json index 45d24a8..a468f73 100644 --- a/io.github.fsobolev.Cavalier.json +++ b/io.github.fsobolev.Cavalier.json @@ -25,7 +25,9 @@ ], "modules" : [ "shared-modules/linux-audio/fftw3f.json", - { + "build-aux/python3-glcontext.json", + "build-aux/python3-moderngl.json", + { "name" : "iniparser", "buildsystem" : "simple", "build-commands" : diff --git a/src/gl_area.py b/src/gl_area.py new file mode 100644 index 0000000..d80092e --- /dev/null +++ b/src/gl_area.py @@ -0,0 +1,155 @@ +# gl_area.py +# +# Copyright 2022 Fyodor Sobolev +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Except as contained in this notice, the name(s) of the above copyright +# holders shall not be used in advertising or otherwise to promote the sale, +# use or other dealings in this Software without prior written +# authorization. +# +# SPDX-License-Identifier: MIT + +from gi.repository import Gtk, GObject +from threading import Thread +from cavalier.cava import Cava +from cavalier.draw_functions import wave, levels, particles, spine, bars +from cavalier.settings import CavalierSettings + +import moderngl +import cairo + +class CavalierGLArea(Gtk.GLArea): + __gtype_name__ = 'CavalierGLArea' + + def __init__(self, settings, **kwargs): + super().__init__(**kwargs) + + def new(): + cgla = Gtk.GLArea.new() + cgla.__class__ = CavalierGLArea + cgla.set_vexpand(True) + cgla.set_hexpand(True) + cgla.cava = None + cgla.spinner = None + cgla.settings = CavalierSettings.new(cgla.on_settings_changed) + cgla.connect('realize', cgla.on_realize) + cgla.connect('render', cgla.on_render) + cgla.connect('unrealize', cgla.on_unrealize) + + return cgla + + def run(self): + print('Using GPU') + self.on_settings_changed(None) + if self.cava == None: + self.cava = Cava() + self.cava_thread = Thread(target=self.cava.run) + self.cava_thread.start() + if self.spinner != None: + self.spinner.set_visible(False) + GObject.timeout_add(1000.0 / 60.0, self.render) + + def on_settings_changed(self, key): + self.draw_mode = self.settings['mode'] + self.set_margin_top(self.settings['margin']) + self.set_margin_bottom(self.settings['margin']) + self.set_margin_start(self.settings['margin']) + self.set_margin_end(self.settings['margin']) + self.offset = self.settings['items-offset'] + self.roundness = self.settings['items-roundness'] + self.thickness = self.settings['line-thickness'] + self.fill = self.settings['fill'] + self.reverse_order = self.settings['reverse-order'] + self.channels = self.settings['channels'] + try: + color_profile = self.settings['color-profiles'][ \ + self.settings['active-color-profile']] + self.colors = color_profile[1] + except: + self.colors = [] + if len(self.colors) == 0: + self.settings['color-profiles'] = [(_('Default'), \ + [(53, 132, 228, 1.0)], [])] + return + + if key in ('bars', 'autosens', 'sensitivity', 'channels', \ + 'smoothing', 'noise-reduction', 'gpu-accel'): + if not self.cava.restarting: + self.cava.stop() + self.cava.restarting = True + if self.spinner != None: + self.spinner.set_visible(True) + self.cava.sample = [] + GObject.timeout_add_seconds(3, self.run) + + def on_realize(self, area): + area.make_current() + self.ctx = moderngl.create_context(standalone=True) + + def render(self): + self.on_render(self, self.ctx) + return True + + def on_render(self, area, context): + self.queue_render() + self.cava_sample = self.cava.sample + if self.reverse_order: + if self.channels == 'mono': + self.cava_sample = self.cava_sample[::-1] + else: + self.cava_sample = \ + self.cava_sample[0:int(len(self.cava_sample)/2):][::-1] + \ + self.cava_sample[int(len(self.cava_sample)/2)::][::-1] + + self.texture = self.render_to_texture(area.get_width(), area.get_height()) + self.texture.use() + #self.screen_rectangle.render(mode=moderngl.TRIANGLE_STRIP) + + def on_unrealize(self, area): + self.cava.stop() + self.texture.release() + + def render_to_texture(self, width, height): + + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) + ctx = cairo.Context(surface) + + if len(self.cava_sample) > 0: + if self.draw_mode == 'wave': + wave(self.cava_sample, ctx, width, height, self.colors, \ + self.fill, self.thickness) + elif self.draw_mode == 'levels': + levels(self.cava_sample, ctx, width, height, self.colors, \ + self.offset, self.roundness, self.fill, self.thickness) + elif self.draw_mode == 'particles': + particles(self.cava_sample, ctx, width, height, self.colors, \ + self.offset, self.roundness, self.fill, self.thickness) + elif self.draw_mode == 'spine': + spine(self.cava_sample, ctx, width, height, self.colors, \ + self.offset, self.roundness, self.fill, self.thickness) + elif self.draw_mode == 'bars': + bars(self.cava_sample, ctx, width, height, self.colors, \ + self.offset, self.fill, self.thickness) + + texture = self.ctx.texture((width, height), 4, data=surface.get_data()) + texture.swizzle = 'BGRA' # use Cairo channel order (alternatively, the shader could do the swizzle) + return texture diff --git a/src/preferences_window.py b/src/preferences_window.py index c1dabb9..2a443b4 100644 --- a/src/preferences_window.py +++ b/src/preferences_window.py @@ -198,6 +198,17 @@ def create_cavalier_page(self): self.pref_autohide_header_switch) self.window_group.add(self.pref_autohide_header) + self.pref_gpu_accel = Adw.ActionRow.new() + self.pref_gpu_accel.set_title(_('GPU acceleration (experimental)')) + self.pref_gpu_accel.set_subtitle( \ + _('Whether to render the visualization using the GPU.')) + self.pref_gpu_accel_switch = Gtk.Switch.new() + self.pref_gpu_accel_switch.set_valign(Gtk.Align.CENTER) + self.pref_gpu_accel.add_suffix(self.pref_gpu_accel_switch) + self.pref_gpu_accel.set_activatable_widget( \ + self.pref_gpu_accel_switch) + self.window_group.add(self.pref_gpu_accel) + self.box_import_export = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 12) self.box_import_export.set_halign(Gtk.Align.CENTER) self.box_import_export.set_margin_top(24) @@ -413,6 +424,8 @@ def load_settings(self): self.settings['window-controls']) self.pref_autohide_header_switch.set_active( \ self.settings['autohide-header']) + self.pref_gpu_accel_switch.set_active( \ + self.settings['gpu-accel']) self.cava_bars_scale.set_value(self.settings['bars']) self.autosens_switch.set_active(self.settings['autosens']) @@ -483,6 +496,9 @@ def bind_settings(self): self.pref_autohide_header_switch.connect('notify::state', \ lambda *args : self.save_setting(self.pref_autohide_header_switch, \ 'autohide-header', self.pref_autohide_header_switch.get_state())) + self.pref_gpu_accel_switch.connect('notify::state', \ + lambda *args : self.save_setting(self.pref_gpu_accel_switch, \ + 'gpu-accel', self.pref_gpu_accel_switch.get_state())) self.cava_bars_scale.connect('value-changed', self.change_bars_count) # `notify::state` signal returns additional parameter that diff --git a/src/window.py b/src/window.py index 61c6923..22e69cb 100644 --- a/src/window.py +++ b/src/window.py @@ -32,6 +32,7 @@ from cavalier.settings import CavalierSettings from cavalier.drawing_area import CavalierDrawingArea +from cavalier.gl_area import CavalierGLArea from cavalier.shortcuts import add_shortcuts @@ -97,6 +98,8 @@ def build_ui(self): self.bin_spinner.set_child(self.spinner) self.drawing_area = CavalierDrawingArea.new() + if self.settings['gpu-accel']: + self.drawing_area = CavalierGLArea.new() self.drawing_area.spinner = self.spinner self.drawing_area.run() self.overlay.set_child(self.drawing_area)