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)