removed fallback c method as it doesn't work for most setups

This commit is contained in:
grimsace
2026-05-19 11:18:48 -05:00
parent 077859a068
commit 8e00f14cf5
+76 -234
View File
@@ -1,287 +1,122 @@
""" """
Solid Noise Generator — Krita Plugin Solid Noise Generator — Krita Plugin
Generates Perlin noise at canvas resolution with configurable octaves (1-16). Generates Perlin noise at canvas resolution with configurable octaves (1-16).
Speed strategy: compile a tiny C shared library at first use via gcc + ctypes.
This gives near-native performance (~0.1s at 1080p, 4 octaves) while requiring
zero third-party Python packages. If gcc is unavailable we fall back to a
pure-Python path automatically.
""" """
import array
import ctypes
import math import math
import os
import random import random
import subprocess
import tempfile
from krita import (Krita, Extension, DockWidget, from krita import DockWidget, DockWidgetFactory, DockWidgetFactoryBase, Extension, Krita
DockWidgetFactory, DockWidgetFactoryBase) from PyQt5.QtCore import QObject, Qt, QThread, pyqtSignal
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QCheckBox,
QSlider, QSpinBox, QGroupBox, QProgressBar, QDoubleSpinBox, QDoubleSpinBox,
QCheckBox, QFrame, QFrame,
QGroupBox,
QHBoxLayout,
QLabel,
QProgressBar,
QPushButton,
QSlider,
QSpinBox,
QVBoxLayout,
QWidget,
) )
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QObject
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# C source for the noise kernel # Noise generation (Pure Python)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
_C_SOURCE = r"""
#include <math.h>
#include <string.h>
static int perm[512]; def _fade(t):
void sn_set_perm(int *p) {
memcpy(perm, p, 512 * sizeof(int));
}
static float fade(float t) { return t*t*t*(t*(t*6.0f-15.0f)+10.0f); }
static float grad(int h, float x, float y) {
h &= 3;
if (h == 0) return x + y;
if (h == 1) return -x + y;
if (h == 2) return x - y;
return -x - y;
}
/* Fill `out` (length=width) with one row of Perlin noise at height-coord ny. */
void sn_row(float *out, int width, float ny, float scale) {
int yi = (int)floorf(ny) & 255;
float yf = ny - floorf(ny);
float yf1 = yf - 1.0f;
float v = fade(yf);
float inv_w = scale / (float)width;
for (int x = 0; x < width; x++) {
float nx = x * inv_w;
int xi = (int)floorf(nx) & 255;
float xf = nx - floorf(nx);
float xf1 = xf - 1.0f;
float u = fade(xf);
int aa = perm[perm[xi ] + yi ];
int ab = perm[perm[xi ] + yi+1];
int ba = perm[perm[xi+1] + yi ];
int bb = perm[perm[xi+1] + yi+1];
float x1 = grad(aa,xf,yf) + u*(grad(ba,xf1,yf) - grad(aa,xf,yf));
float x2 = grad(ab,xf,yf1) + u*(grad(bb,xf1,yf1) - grad(ab,xf,yf1));
out[x] = x1 + v*(x2 - x1);
}
}
/*
* Generate the full canvas (height rows x width cols) for one octave and
* ADD the result * amplitude into acc[].
*/
void sn_octave(float *acc, int width, int height,
float scale, float amplitude) {
float inv_h = scale / (float)height;
float *row = (float *)__builtin_alloca(width * sizeof(float));
for (int y = 0; y < height; y++) {
float ny = y * inv_h;
sn_row(row, width, ny, scale);
int base = y * width;
for (int x = 0; x < width; x++)
acc[base + x] += row[x] * amplitude;
}
}
/*
* Normalise acc[], stretch so darkest=0 and brightest=255, pack as BGRA.
* max_amp = sum of all octave amplitudes.
* invert = 0 or 1.
*/
void sn_pack_bgra(float *acc, unsigned char *out,
int npixels, float max_amp, int invert) {
float inv = 1.0f / max_amp;
/* First pass: normalise to [0,1] and find actual min/max. */
float mn = 1.0f, mx = 0.0f;
for (int i = 0; i < npixels; i++) {
float v = (acc[i] * inv + 1.0f) * 0.5f;
if (v < 0.0f) v = 0.0f;
else if (v > 1.0f) v = 1.0f;
acc[i] = v;
if (v < mn) mn = v;
if (v > mx) mx = v;
}
/* Second pass: remap [mn,mx] -> [0,255] and pack. */
float range = mx - mn;
float scale = (range > 1e-6f) ? (255.0f / range) : 0.0f;
for (int i = 0; i < npixels; i++) {
float v = (acc[i] - mn) * scale;
if (invert) v = 255.0f - v;
unsigned char bv = (unsigned char)(v + 0.5f);
int base = i * 4;
out[base ] = bv;
out[base+1] = bv;
out[base+2] = bv;
out[base+3] = 255;
}
}
"""
# ---------------------------------------------------------------------------
# Build / load the shared library (once per session)
# ---------------------------------------------------------------------------
_clib = None # ctypes.CDLL or None
_clib_dir = None # tempfile.TemporaryDirectory kept alive
def _try_build_clib():
global _clib, _clib_dir
if _clib is not None:
return True
try:
_clib_dir = tempfile.TemporaryDirectory(prefix="krita_solid_noise_")
src_path = os.path.join(_clib_dir.name, "sn.c")
lib_path = os.path.join(_clib_dir.name, "sn.so")
with open(src_path, "w") as f:
f.write(_C_SOURCE)
result = subprocess.run(
["gcc", "-O2", "-shared", "-fPIC", "-o", lib_path, src_path, "-lm"],
capture_output=True, timeout=30
)
if result.returncode != 0:
return False
lib = ctypes.CDLL(lib_path)
lib.sn_set_perm.argtypes = [ctypes.POINTER(ctypes.c_int)]
lib.sn_set_perm.restype = None
lib.sn_octave.argtypes = [ctypes.POINTER(ctypes.c_float),
ctypes.c_int, ctypes.c_int,
ctypes.c_float, ctypes.c_float]
lib.sn_octave.restype = None
lib.sn_pack_bgra.argtypes = [ctypes.POINTER(ctypes.c_float),
ctypes.POINTER(ctypes.c_ubyte),
ctypes.c_int, ctypes.c_float, ctypes.c_int]
lib.sn_pack_bgra.restype = None
_clib = lib
return True
except Exception:
return False
# ---------------------------------------------------------------------------
# Pure-Python fallback (used when gcc is unavailable)
# ---------------------------------------------------------------------------
def _py_fade(t):
return t * t * t * (t * (t * 6.0 - 15.0) + 10.0) return t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
def _py_grad(h, x, y):
def _grad(h, x, y):
h &= 3 h &= 3
if h == 0: return x + y if h == 0:
if h == 1: return -x + y return x + y
if h == 2: return x - y if h == 1:
return -x + y
if h == 2:
return x - y
return -x - y return -x - y
def _py_generate(width, height, octaves, scale, seed, invert, progress_cb):
def generate(width, height, octaves, scale, seed, invert, progress_cb):
perms = [] perms = []
for i in range(octaves): for i in range(octaves):
rng = random.Random(seed + i) rng = random.Random(seed + i)
p = list(range(256)); rng.shuffle(p) p = list(range(256))
rng.shuffle(p)
perms.append(p + p) perms.append(p + p)
amps = [0.5 ** i for i in range(octaves)] amps = [0.5**i for i in range(octaves)]
freqs = [2.0 ** i for i in range(octaves)] freqs = [2.0**i for i in range(octaves)]
max_a = sum(amps) max_a = sum(amps)
acc = [0.0] * (width * height) acc = [0.0] * (width * height)
for oi in range(octaves): for oi in range(octaves):
p = perms[oi]; amp = amps[oi]; fs = scale * freqs[oi] p = perms[oi]
inv_h = fs / height; inv_w = fs / width amp = amps[oi]
fs = scale * freqs[oi]
inv_h = fs / height
inv_w = fs / width
for y in range(height): for y in range(height):
ny = y * inv_h ny = y * inv_h
nyi = int(math.floor(ny)) & 255 nyi = int(math.floor(ny)) & 255
yf = ny - math.floor(ny); yf1 = yf - 1.0; v = _py_fade(yf) yf = ny - math.floor(ny)
yf1 = yf - 1.0
v = _fade(yf)
base = y * width base = y * width
for x in range(width): for x in range(width):
nx = x * inv_w nx = x * inv_w
xi = int(math.floor(nx)) & 255 xi = int(math.floor(nx)) & 255
xf = nx - math.floor(nx); xf1 = xf - 1.0; u = _py_fade(xf) xf = nx - math.floor(nx)
aa=p[p[xi]+nyi]; ab=p[p[xi]+nyi+1] xf1 = xf - 1.0
ba=p[p[xi+1]+nyi]; bb=p[p[xi+1]+nyi+1] u = _fade(xf)
x1 = _py_grad(aa,xf,yf) + u*(_py_grad(ba,xf1,yf) - _py_grad(aa,xf,yf)) aa = p[p[xi] + nyi]
x2 = _py_grad(ab,xf,yf1) + u*(_py_grad(bb,xf1,yf1) - _py_grad(ab,xf,yf1)) ab = p[p[xi] + nyi + 1]
acc[base+x] += (x1 + v*(x2-x1)) * amp ba = p[p[xi + 1] + nyi]
progress_cb(int((oi+1)/octaves*90)) bb = p[p[xi + 1] + nyi + 1]
x1 = _grad(aa, xf, yf) + u * (_grad(ba, xf1, yf) - _grad(aa, xf, yf))
x2 = _grad(ab, xf, yf1) + u * (_grad(bb, xf1, yf1) - _grad(ab, xf, yf1))
acc[base + x] += (x1 + v * (x2 - x1)) * amp
progress_cb(int((oi + 1) / octaves * 90))
inv_norm = 1.0 / max_a inv_norm = 1.0 / max_a
# Normalise to [0,1] # Normalise to [0,1]
norm = [] norm = []
for v in acc: for v in acc:
v = (v * inv_norm + 1.0) * 0.5 v = (v * inv_norm + 1.0) * 0.5
if v < 0.0: v = 0.0 if v < 0.0:
elif v > 1.0: v = 1.0 v = 0.0
elif v > 1.0:
v = 1.0
norm.append(v) norm.append(v)
# Stretch so min→0 and max→1 # Stretch so min→0 and max→1
mn = min(norm); mx = max(norm) mn = min(norm)
mx = max(norm)
rng = mx - mn rng = mx - mn
scale = (1.0 / rng) if rng > 1e-6 else 0.0 scale = (1.0 / rng) if rng > 1e-6 else 0.0
pixels = bytearray(width * height * 4) pixels = bytearray(width * height * 4)
for i in range(width * height): for i in range(width * height):
v = (norm[i] - mn) * scale v = (norm[i] - mn) * scale
if invert: v = 1.0 - v if invert:
v = 1.0 - v
bv = int(v * 255.0 + 0.5) bv = int(v * 255.0 + 0.5)
b = i * 4 b = i * 4
pixels[b] = pixels[b+1] = pixels[b+2] = bv; pixels[b+3] = 255 pixels[b] = pixels[b + 1] = pixels[b + 2] = bv
pixels[b + 3] = 255
progress_cb(100) progress_cb(100)
return bytes(pixels) return bytes(pixels)
# ---------------------------------------------------------------------------
# C-accelerated path
# ---------------------------------------------------------------------------
def _c_generate(width, height, octaves, scale, seed, invert, progress_cb):
lib = _clib
npix = width * height
acc_arr = (ctypes.c_float * npix)(*([0.0] * npix))
bgra_arr = (ctypes.c_ubyte * (npix * 4))()
amps = [0.5 ** i for i in range(octaves)]
freqs = [2.0 ** i for i in range(octaves)]
max_a = sum(amps)
for oi in range(octaves):
rng = random.Random(seed + oi)
p = list(range(256)); rng.shuffle(p); p = p * 2
perm_arr = (ctypes.c_int * 512)(*p)
lib.sn_set_perm(perm_arr)
lib.sn_octave(acc_arr, width, height,
ctypes.c_float(scale * freqs[oi]),
ctypes.c_float(amps[oi]))
progress_cb(int((oi + 1) / octaves * 90))
lib.sn_pack_bgra(acc_arr, bgra_arr, npix,
ctypes.c_float(max_a), ctypes.c_int(1 if invert else 0))
progress_cb(100)
return bytes(bgra_arr)
def generate(width, height, octaves, scale, seed, invert, progress_cb):
if _try_build_clib():
return _c_generate(width, height, octaves, scale, seed, invert, progress_cb)
return _py_generate(width, height, octaves, scale, seed, invert, progress_cb)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Worker # Worker
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class NoiseWorker(QObject): class NoiseWorker(QObject):
progress = pyqtSignal(int) progress = pyqtSignal(int)
finished = pyqtSignal(bytes, int, int) finished = pyqtSignal(bytes, int, int)
@@ -298,9 +133,15 @@ class NoiseWorker(QObject):
def run(self): def run(self):
try: try:
data = generate(self.width, self.height, self.octaves, data = generate(
self.scale, self.seed, self.invert, self.width,
self.progress.emit) self.height,
self.octaves,
self.scale,
self.seed,
self.invert,
self.progress.emit,
)
self.finished.emit(data, self.width, self.height) self.finished.emit(data, self.width, self.height)
except Exception as e: except Exception as e:
self.error.emit(str(e)) self.error.emit(str(e))
@@ -422,11 +263,14 @@ class SolidNoiseDock(DockWidget):
self._set_busy(True) self._set_busy(True)
self._thread = QThread() self._thread = QThread()
self._worker = NoiseWorker(w, h, self._worker = NoiseWorker(
w,
h,
self._oct_slider.value(), self._oct_slider.value(),
self._scale_spin.value(), self._scale_spin.value(),
self._seed_spin.value(), self._seed_spin.value(),
self._invert_chk.isChecked()) self._invert_chk.isChecked(),
)
self._worker.moveToThread(self._thread) self._worker.moveToThread(self._thread)
self._thread.started.connect(self._worker.run) self._thread.started.connect(self._worker.run)
self._worker.progress.connect(self._bar.setValue) self._worker.progress.connect(self._bar.setValue)
@@ -451,10 +295,7 @@ class SolidNoiseDock(DockWidget):
layer = doc.activeNode() layer = doc.activeNode()
layer.setPixelData(data, 0, 0, w, h) layer.setPixelData(data, 0, 0, w, h)
doc.refreshProjection() doc.refreshProjection()
accel = "C" if _clib else "Python" self._status.setText(f"Done {w}x{h}, {self._oct_slider.value()} oct")
self._status.setText(
f"Done {w}x{h}, {self._oct_slider.value()} oct [{accel}]"
)
except Exception as e: except Exception as e:
self._on_error(str(e)) self._on_error(str(e))
finally: finally:
@@ -472,6 +313,7 @@ class SolidNoiseDock(DockWidget):
# Extension + docker registration # Extension + docker registration
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class SolidNoisePlugin(Extension): class SolidNoisePlugin(Extension):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)