Experimental WebGPU + PIP plugin
I have toyed a bit with WebGPU (wgpu rust implementaion and it’s python binding)
Also plugin uses Pip to install required packages, and if Pip it self is missing, it also will be installed.
About WebGPU:
- it’s easier than Vulcan.
- It’s multi-platform (can use OpenGL, Vulcan, DirectX and Metal as backend.)
- It`s fresh, without 10+years of legacy code.
- When DockWidget is docked to Krita’s MainWindow canvas will have insane lag (without animation updates there is no lag)
About Pip
- packages are installed on shared users package-site folder (packages will be shared with system python of same version.)
request.urlopen(...)is used to download latest Pip wheel frompypi.orgsite (on macOS ssl.SSLContext() would be required, but this test code lacks that.)- But if you really wish to test this on macOS,
pip-25.0.1-py3-none-any.whlcan be manually downloaded frompypi.org/pypi/pipand placed in userpykritafolder)
File tree
pykrita/
hello_wgpu_plugin/
__init__.py
pip_for_krita.py
hello_wgpu_docker.py
hello_wgpu_plugin.desktop
hello_wgpu_plugin/__init__.py
from krita import (
Krita,
DockWidgetFactory,
DockWidgetFactoryBase)
PLUGIN_NAME = __name__
def ensure_dependencies():
""" check is wgpu available. If not, use pip to get it. """
try:
import wgpu
except ImportError:
from .pip_for_krita import pip_install
pip_install(packages=['wgpu'])
def register():
ensure_dependencies()
from .hello_wgpu_docker import HelloWgpuDocker
app = Krita.instance()
app.addDockWidgetFactory(DockWidgetFactory(
f'{PLUGIN_NAME}:{HelloWgpuDocker.__qualname__}',
DockWidgetFactoryBase.DockRight,
HelloWgpuDocker))
register()
hello_wgpu_plugin/pip_for_krita.py
import sys
import site
import json
import runpy
import io
from pathlib import Path
from urllib import request
from krita import Krita
def ensure_pip():
# make sure that user site backages exists and is in sys.path
user_site_packages = site.getusersitepackages()
if user_site_packages not in sys.path:
Path(user_site_packages).mkdir(parents=True, exist_ok=True)
site.addsitedir(user_site_packages)
# test is pip currently available
try:
import pip
return # pip available, nothing to do
except ImportError:
pass # no pip, try to get it
user_pykrita_path = Path(Krita.instance().getAppDataLocation()) / 'pykrita'
existing_pip_wheel_path = next(user_pykrita_path.glob('pip-*.whl'), None)
if existing_pip_wheel_path:
# pip wheel found, making sure that it's in sys.path
existing_pip_wheel_path_str = str(existing_pip_wheel_path)
if existing_pip_wheel_path_str not in sys.path:
sys.path.append(existing_pip_wheel_path_str)
print(f'Existing Pip wheel "{existing_pip_wheel_path}" added to sys.path')
return
else:
# get available Pip releases from pypi site
pip_releases_json = json.loads(request.urlopen('https://pypi.org/pypi/pip/json').read())
filename = None
latest_pip_download_url = None
for url in pip_releases_json['urls']:
if url['packagetype'] == 'bdist_wheel':
filename = url['filename']
latest_pip_download_url = url['url']
# download pip package
pip_wheel_path = user_pykrita_path / filename
pip_wheel_path.write_bytes(request.urlopen(latest_pip_download_url).read())
sys.path.append(str(pip_wheel_path))
print(f'Pip wheel "{pip_wheel_path}" downloaded and added to sys.path')
def pip_install(packages=None, requirement=None, upgrade=False):
pip_cmd = ['pip', 'install', '--user']
if upgrade:
pip_cmd += ['--upgrade']
if packages is not None:
pip_cmd += packages
if requirement is not None:
pip_cmd += ['--requirement', requirement]
return _run_pip(pip_cmd)
def pip_inspect():
stdout, stderr = _run_pip(['pip', 'inspect', '--user'])
return json.loads(stdout)
def _run_pip(pip_cmd):
ensure_pip()
def _dummy_exit_func(_exit_code=0):
return _exit_code
# store original args, exit func, stdout, stderr
old_argv = sys.argv
old_exit = sys.exit
old_sys_stdout = sys.stdout
old_sys_stderr = sys.stderr
try:
stdout_buffer = io.StringIO()
stderr_buffer = io.StringIO()
sys.exit = _dummy_exit_func
sys.argv = pip_cmd
sys.stdout = stdout_buffer
sys.stderr = stderr_buffer
runpy.run_module('pip', run_name='__main__')
return stdout_buffer.getvalue(), stderr_buffer.getvalue()
finally:
# restore original args, exit func, stdout, stderr
sys.exit = old_argv
sys.argv = old_exit
sys.stdout = old_sys_stdout
sys.stderr = old_sys_stderr
hello_wgpu_plugin/hello_wgpu_docker.py
import time
from struct import Struct
from krita import Krita, DockWidget
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtWidgets import QWidget, QLabel, QVBoxLayout
import wgpu
from wgpu.gui.qt import WgpuWidget
shader_art_1 = """\
// https://pongasoft.github.io/webgpu-shader-toy/ (Shader Art 1)
// Ported from Shader Art Coding (https://www.youtube.com/watch?v=f4s1h2YETNY)
struct ShaderToyInputs {
size: vec4f,
mouse: vec4f,
time: f32,
frame: i32
};
@vertex
fn vs_main(@builtin(vertex_index) i : u32) -> @builtin(position) vec4f {
const pos = array(vec2f(-1, 1), vec2f(1, 1), vec2f(-1, -1), vec2f(1, -1));
return vec4f(pos[i], 0, 1);
};
@group(0) @binding(0)
var<uniform> inputs: ShaderToyInputs;
fn palette(t: f32) -> vec3f {
const a = vec3f(0.5, 0.5, 0.5);
const b = vec3f(0.5, 0.5, 0.5);
const c = vec3f(1.0, 1.0, 1.0);
const d = vec3f(0.263, 0.416, 0.557);
return a + (b * cos(6.28318 * ((c*t) + d)));
};
@fragment
fn fs_main(@builtin(position) pos: vec4f) -> @location(0) vec4f {
var uv = (pos.xy * 2.0 - inputs.size.xy) / inputs.size.y;
let uv0 = uv;
var finalColor = vec3f(0.0);
for(var i = 0; i < 4; i++) {
uv = fract(uv * 1.5) - 0.5;
var d = length(uv) * exp(-length(uv0));
var col = palette(length(uv0) + f32(i) * 0.4 + inputs.time * 0.4);
d = sin(d * 8.0 + inputs.time) / 8.0;
d = abs(d);
d = pow(0.01 / d, 1.2);
finalColor += col * d;
}
return vec4f(finalColor, 1.0);
};
"""
class HelloWgpuDocker(DockWidget):
def __init__(self):
super().__init__()
self._frame = 0
self._start_time = time.time()
self.setWindowTitle('Hello WGPU')
self._init_ui()
self._init_wgpu()
def _init_ui(self):
self._content_widget = QWidget()
layout = QVBoxLayout()
self._content_widget.setContentsMargins(6, 6, 6, 6)
self._content_widget.setLayout(layout)
self._wgpu_canvas = WgpuWidget(minimumSize=QSize(200, 200), parent=self._content_widget)
layout.addWidget(self._wgpu_canvas, stretch=100)
self._author_label = QLabel(
text=('<a href="https://www.shadertoy.com/view/mtyGWy">'
'<font face="DejaVu Sans" color="#8888FF" size="5"><i>'
'Original created by kishimisu'
'</i></font></a>'),
textFormat=Qt.RichText,
textInteractionFlags=Qt.TextBrowserInteraction,
openExternalLinks=True,
alignment=Qt.AlignRight | Qt.AlignBaseline,
parent=self._content_widget)
layout.addWidget(self._author_label)
self.setWidget(self._content_widget)
def _init_wgpu(self):
adapter = wgpu.gpu.request_adapter_sync(power_preference='high-performance')
self._device = adapter.request_device_sync(required_limits=None)
context = self._wgpu_canvas.get_context('wgpu')
render_texture_format = context.get_preferred_format(self._device.adapter)
context.configure(device=self._device, format=render_texture_format)
shader = self._device.create_shader_module(code=shader_art_1)
self._render_pipeline = self._device.create_render_pipeline(
layout='auto',
depth_stencil=None,
multisample=None,
primitive={
'topology': wgpu.PrimitiveTopology.triangle_strip,
'front_face': wgpu.FrontFace.ccw,
'cull_mode': wgpu.CullMode.none},
vertex={
'module': shader,
'entry_point': 'vs_main'},
fragment={
'module': shader,
'entry_point': 'fs_main',
'targets': [
{
'format': render_texture_format,
'blend': {'color': {}, 'alpha': {}}
}
]}
)
self._init_uniforms()
self._wgpu_canvas.request_draw(self.draw_wgpu)
def _init_uniforms(self):
self._size_struct = Struct('4f') # offset 0, size 16 bytes
self._mouse_struct = Struct('4f') # offset 16, size 16 bytes
self._time_struct = Struct('f') # offset 32, size 4 bytes
self._frame_struct = Struct('i') # offset 36, size 4 bytes
self._pad_8_struct = Struct('8x') # offset 40, size 8 bytes
self._uniform_data = bytearray(
[0] * (self._size_struct.size
+ self._mouse_struct.size
+ self._time_struct.size
+ self._frame_struct.size
+ self._pad_8_struct.size))
self._uniform_buffer = self._device.create_buffer(
size=len(self._uniform_data),
usage=wgpu.BufferUsage.UNIFORM | wgpu.BufferUsage.COPY_DST)
bind_groups_layout_entries = [
{
'binding': 0,
'resource': {
'buffer': self._uniform_buffer,
'offset': 0,
'size': len(self._uniform_data)
}
}
]
binding_layout = [
{
'binding': 0,
'visibility': wgpu.ShaderStage.FRAGMENT,
'buffer': {'type': wgpu.BufferBindingType.uniform}
}
]
self._bind_group_layout = self._device.create_bind_group_layout(entries=binding_layout)
self._bind_group = self._device.create_bind_group(
layout=self._render_pipeline.get_bind_group_layout(0),
entries=bind_groups_layout_entries)
def draw_wgpu(self):
self._size_struct.pack_into(self._uniform_data, 0,
float(self._wgpu_canvas.width()), float(self._wgpu_canvas.height()), 1.0, 1.0)
self._mouse_struct.pack_into(self._uniform_data, 16, 100.0, 100.0, -1.0, -1.0)
self._time_struct.pack_into(self._uniform_data, 32, time.time() - self._start_time)
self._frame_struct.pack_into(self._uniform_data, 36, self._frame)
self._device.queue.write_buffer(self._uniform_buffer, 0, self._uniform_data)
current_texture = self._wgpu_canvas.get_context('wgpu').get_current_texture()
command_encoder = self._device.create_command_encoder()
render_pass = command_encoder.begin_render_pass(
color_attachments=[
{
'view': current_texture.create_view(),
'resolve_target': None,
'clear_value': (0, 0, 0, 1),
'load_op': wgpu.LoadOp.clear,
'store_op': wgpu.StoreOp.store
}
])
render_pass.set_pipeline(self._render_pipeline)
render_pass.set_bind_group(0, self._bind_group)
render_pass.draw(4)
render_pass.end()
self._device.queue.submit([command_encoder.finish()])
self._frame += 1
# request next frame
self._wgpu_canvas.update()
def canvasChanged(self, canvas):
pass
hello_wgpu_plugin.desktop
[Desktop Entry]
Type=Service
ServiceTypes=Krita/PythonPlugin
X-KDE-Library=hello_wgpu_plugin
X-Python-2-Compatible=false
X-Krita-Manual=readme.md
Name=Hello WGPU plugin
Comment=Experimental plugin for testing WGPU in Krita, also uses pip_for_krita to install depending packages.
Have fun!
Edit: "Original" created by kishimisu added to crediting text.
/AkiR
