Effector: C++/CMake image effects app
Port of the Python prototype to C++20 with a CMake build: - effector_core: GUI-free static library (effects, image IO via gdk-pixbuf, declarative param specs, registry, portable seeded RNG, thread helper) - scanlines effect: plans strips from the seeded RNG, then copies pixels in parallel with memcpy and doubling horizontal tiling - gtkmm 4 interface: native file dialogs, widgets generated from the param specs, coalesced background renders, preview as a GDK memory texture - CLI: flags generated from the effect registry, headless rendering with no display required - ctest suite covering the RNG, registry, effect geometry and image round trip The Python prototype is kept under python-reference/.
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
#include "core/effect.hpp"
|
||||
|
||||
namespace effector {
|
||||
|
||||
ParamValues Effect::defaults() const {
|
||||
ParamValues values;
|
||||
for (const auto& spec : params()) {
|
||||
switch (spec.type) {
|
||||
case ParamSpec::Type::Int:
|
||||
values[spec.key] = static_cast<long>(spec.defaultValue);
|
||||
break;
|
||||
case ParamSpec::Type::Double:
|
||||
values[spec.key] = spec.defaultValue;
|
||||
break;
|
||||
case ParamSpec::Type::Bool:
|
||||
values[spec.key] = spec.defaultValue != 0.0;
|
||||
break;
|
||||
case ParamSpec::Type::Choice:
|
||||
values[spec.key] = spec.defaultChoice;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
} // namespace effector
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/image.hpp"
|
||||
#include "core/params.hpp"
|
||||
#include "core/rng.hpp"
|
||||
|
||||
namespace effector {
|
||||
|
||||
class EffectError : public std::runtime_error {
|
||||
public:
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
// Base class for every effect. Canvas size and the random source are common to
|
||||
// all of them; params() covers only the effect-specific knobs.
|
||||
class Effect {
|
||||
public:
|
||||
virtual ~Effect() = default;
|
||||
|
||||
virtual std::string name() const = 0;
|
||||
virtual std::string title() const = 0;
|
||||
virtual std::string description() const = 0;
|
||||
virtual const std::vector<ParamSpec>& params() const = 0;
|
||||
|
||||
virtual Image render(const std::vector<Image>& sources, int width, int height, Rng& rng,
|
||||
const ParamValues& values) const = 0;
|
||||
|
||||
ParamValues defaults() const;
|
||||
};
|
||||
|
||||
using EffectPtr = std::shared_ptr<const Effect>;
|
||||
|
||||
} // namespace effector
|
||||
@@ -0,0 +1,140 @@
|
||||
#include "core/image.hpp"
|
||||
|
||||
#include <gdk-pixbuf/gdk-pixbuf.h>
|
||||
|
||||
#include "core/parallel.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace effector {
|
||||
namespace {
|
||||
|
||||
// The plain C pixbuf API is used on purpose: it needs no gtkmm wrapper
|
||||
// registration and no display, so the core renders headlessly.
|
||||
using PixbufPtr = std::unique_ptr<GdkPixbuf, decltype(&g_object_unref)>;
|
||||
|
||||
PixbufPtr wrap(GdkPixbuf* pixbuf) { return PixbufPtr(pixbuf, &g_object_unref); }
|
||||
|
||||
std::string takeError(GError* error) {
|
||||
std::string message = error != nullptr && error->message != nullptr ? error->message
|
||||
: "unknown error";
|
||||
if (error != nullptr) g_error_free(error);
|
||||
return message;
|
||||
}
|
||||
|
||||
std::string lowerExtension(const std::string& path) {
|
||||
const auto dot = path.find_last_of('.');
|
||||
if (dot == std::string::npos) return {};
|
||||
std::string ext = path.substr(dot + 1);
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
return ext;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Image::Image(int width, int height)
|
||||
: width_(width), height_(height),
|
||||
pixels_(static_cast<std::size_t>(std::max(width, 0)) * 3 *
|
||||
static_cast<std::size_t>(std::max(height, 0))) {}
|
||||
|
||||
Image Image::load(const std::string& path) {
|
||||
GError* error = nullptr;
|
||||
PixbufPtr pixbuf = wrap(gdk_pixbuf_new_from_file(path.c_str(), &error));
|
||||
if (!pixbuf) throw std::runtime_error(takeError(error));
|
||||
|
||||
if (GdkPixbuf* oriented = gdk_pixbuf_apply_embedded_orientation(pixbuf.get())) {
|
||||
pixbuf = wrap(oriented);
|
||||
}
|
||||
|
||||
const int width = gdk_pixbuf_get_width(pixbuf.get());
|
||||
const int height = gdk_pixbuf_get_height(pixbuf.get());
|
||||
const int channels = gdk_pixbuf_get_n_channels(pixbuf.get());
|
||||
const int srcStride = gdk_pixbuf_get_rowstride(pixbuf.get());
|
||||
const guint8* src = gdk_pixbuf_get_pixels(pixbuf.get());
|
||||
|
||||
Image image(width, height);
|
||||
for (int y = 0; y < height; ++y) {
|
||||
const guint8* in = src + static_cast<std::size_t>(y) * srcStride;
|
||||
std::uint8_t* out = image.row(y);
|
||||
if (channels == 3) {
|
||||
std::memcpy(out, in, static_cast<std::size_t>(width) * 3);
|
||||
} else {
|
||||
// Flatten alpha onto white, matching what a viewer would show.
|
||||
for (int x = 0; x < width; ++x) {
|
||||
const int a = in[x * channels + 3];
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
const int value = in[x * channels + c] * a + 255 * (255 - a);
|
||||
out[x * 3 + c] = static_cast<std::uint8_t>((value + 127) / 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
image.setPath(path);
|
||||
return image;
|
||||
}
|
||||
|
||||
void Image::save(const std::string& path) const {
|
||||
if (empty()) throw std::runtime_error("nothing to save");
|
||||
|
||||
PixbufPtr pixbuf = wrap(gdk_pixbuf_new(GDK_COLORSPACE_RGB, FALSE, 8, width_, height_));
|
||||
if (!pixbuf) throw std::runtime_error("out of memory");
|
||||
|
||||
const int dstStride = gdk_pixbuf_get_rowstride(pixbuf.get());
|
||||
guint8* dst = gdk_pixbuf_get_pixels(pixbuf.get());
|
||||
for (int y = 0; y < height_; ++y) {
|
||||
std::memcpy(dst + static_cast<std::size_t>(y) * dstStride, row(y), stride());
|
||||
}
|
||||
|
||||
const std::string ext = lowerExtension(path);
|
||||
std::string type = "png";
|
||||
if (ext == "jpg" || ext == "jpeg") type = "jpeg";
|
||||
else if (ext == "webp") type = "webp";
|
||||
else if (ext == "tif" || ext == "tiff") type = "tiff";
|
||||
else if (ext == "bmp") type = "bmp";
|
||||
|
||||
GError* error = nullptr;
|
||||
const gboolean saved =
|
||||
type == "jpeg"
|
||||
? gdk_pixbuf_save(pixbuf.get(), path.c_str(), type.c_str(), &error, "quality", "95",
|
||||
nullptr)
|
||||
: gdk_pixbuf_save(pixbuf.get(), path.c_str(), type.c_str(), &error, nullptr);
|
||||
if (saved == FALSE) throw std::runtime_error(takeError(error));
|
||||
}
|
||||
|
||||
std::vector<Image> loadImages(const std::vector<std::string>& paths,
|
||||
std::vector<LoadFailure>& failures) {
|
||||
// Decoding is the dominant cost of a headless run, and the files are
|
||||
// independent, so they are decoded in parallel. Slots keep the result in
|
||||
// the order the paths were given.
|
||||
std::vector<Image> slots(paths.size());
|
||||
std::vector<std::string> errors(paths.size());
|
||||
|
||||
parallelFor(paths.size(), [&](std::size_t begin, std::size_t end) {
|
||||
for (std::size_t i = begin; i < end; ++i) {
|
||||
try {
|
||||
slots[i] = Image::load(paths[i]);
|
||||
} catch (const std::exception& error) {
|
||||
errors[i] = error.what();
|
||||
}
|
||||
}
|
||||
}, 1);
|
||||
|
||||
std::vector<Image> images;
|
||||
images.reserve(paths.size());
|
||||
for (std::size_t i = 0; i < paths.size(); ++i) {
|
||||
if (errors[i].empty()) {
|
||||
images.push_back(std::move(slots[i]));
|
||||
} else {
|
||||
failures.push_back({paths[i], errors[i]});
|
||||
}
|
||||
}
|
||||
return images;
|
||||
}
|
||||
|
||||
} // namespace effector
|
||||
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace effector {
|
||||
|
||||
// Tightly packed 8-bit RGB. Row stride is always width * 3, which lets the
|
||||
// effects work with plain memcpy and hands GDK a buffer it can upload as is.
|
||||
class Image {
|
||||
public:
|
||||
Image() = default;
|
||||
Image(int width, int height);
|
||||
|
||||
int width() const noexcept { return width_; }
|
||||
int height() const noexcept { return height_; }
|
||||
std::size_t stride() const noexcept { return static_cast<std::size_t>(width_) * 3; }
|
||||
bool empty() const noexcept { return width_ <= 0 || height_ <= 0; }
|
||||
|
||||
std::uint8_t* row(int y) noexcept { return pixels_.data() + static_cast<std::size_t>(y) * stride(); }
|
||||
const std::uint8_t* row(int y) const noexcept {
|
||||
return pixels_.data() + static_cast<std::size_t>(y) * stride();
|
||||
}
|
||||
std::uint8_t* data() noexcept { return pixels_.data(); }
|
||||
const std::uint8_t* data() const noexcept { return pixels_.data(); }
|
||||
std::size_t byteSize() const noexcept { return pixels_.size(); }
|
||||
|
||||
const std::string& path() const noexcept { return path_; }
|
||||
void setPath(std::string path) { path_ = std::move(path); }
|
||||
|
||||
// Decodes via GdkPixbuf (PNG, JPEG, WebP, TIFF, BMP, ...), applying the
|
||||
// embedded EXIF orientation. Throws std::runtime_error on failure.
|
||||
static Image load(const std::string& path);
|
||||
|
||||
// Encodes by file extension; png is used when the extension is unknown.
|
||||
void save(const std::string& path) const;
|
||||
|
||||
private:
|
||||
int width_ = 0;
|
||||
int height_ = 0;
|
||||
std::vector<std::uint8_t> pixels_;
|
||||
std::string path_;
|
||||
};
|
||||
|
||||
struct LoadFailure {
|
||||
std::string path;
|
||||
std::string message;
|
||||
};
|
||||
|
||||
// Loads every path, collecting rather than throwing on individual failures.
|
||||
std::vector<Image> loadImages(const std::vector<std::string>& paths,
|
||||
std::vector<LoadFailure>& failures);
|
||||
|
||||
} // namespace effector
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace effector {
|
||||
|
||||
// Splits [0, count) into contiguous chunks across the hardware threads and runs
|
||||
// `work(begin, end)` on each. `grain` is the smallest number of items worth
|
||||
// giving a thread of its own; below that the work runs inline.
|
||||
template <typename Work>
|
||||
void parallelFor(std::size_t count, Work&& work, std::size_t grain = 16) {
|
||||
if (count == 0) return;
|
||||
|
||||
unsigned hardware = std::thread::hardware_concurrency();
|
||||
if (hardware == 0) hardware = 1;
|
||||
const std::size_t workers =
|
||||
std::min<std::size_t>(hardware, (count + grain - 1) / std::max<std::size_t>(grain, 1));
|
||||
if (workers <= 1) {
|
||||
work(std::size_t{0}, count);
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<std::thread> threads;
|
||||
threads.reserve(workers - 1);
|
||||
const std::size_t chunk = (count + workers - 1) / workers;
|
||||
for (std::size_t w = 1; w < workers; ++w) {
|
||||
const std::size_t begin = std::min(count, w * chunk);
|
||||
const std::size_t end = std::min(count, begin + chunk);
|
||||
if (begin >= end) break;
|
||||
threads.emplace_back([&work, begin, end] { work(begin, end); });
|
||||
}
|
||||
work(std::size_t{0}, std::min(count, chunk));
|
||||
for (auto& thread : threads) thread.join();
|
||||
}
|
||||
|
||||
} // namespace effector
|
||||
@@ -0,0 +1,108 @@
|
||||
#include "core/params.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace effector {
|
||||
|
||||
std::string ParamSpec::flag() const {
|
||||
std::string flag = "--" + key;
|
||||
std::replace(flag.begin(), flag.end(), '_', '-');
|
||||
return flag;
|
||||
}
|
||||
|
||||
ParamSpec ParamSpec::makeInt(std::string key, std::string label, long defaultValue, long minimum,
|
||||
long maximum, std::string help, long step) {
|
||||
ParamSpec spec;
|
||||
spec.type = Type::Int;
|
||||
spec.key = std::move(key);
|
||||
spec.label = std::move(label);
|
||||
spec.help = std::move(help);
|
||||
spec.defaultValue = static_cast<double>(defaultValue);
|
||||
spec.minimum = static_cast<double>(minimum);
|
||||
spec.maximum = static_cast<double>(maximum);
|
||||
spec.step = static_cast<double>(step);
|
||||
return spec;
|
||||
}
|
||||
|
||||
ParamSpec ParamSpec::makeDouble(std::string key, std::string label, double defaultValue,
|
||||
double minimum, double maximum, std::string help, double step,
|
||||
int digits) {
|
||||
ParamSpec spec;
|
||||
spec.type = Type::Double;
|
||||
spec.key = std::move(key);
|
||||
spec.label = std::move(label);
|
||||
spec.help = std::move(help);
|
||||
spec.defaultValue = defaultValue;
|
||||
spec.minimum = minimum;
|
||||
spec.maximum = maximum;
|
||||
spec.step = step;
|
||||
spec.digits = digits;
|
||||
return spec;
|
||||
}
|
||||
|
||||
ParamSpec ParamSpec::makeBool(std::string key, std::string label, bool defaultValue,
|
||||
std::string help) {
|
||||
ParamSpec spec;
|
||||
spec.type = Type::Bool;
|
||||
spec.key = std::move(key);
|
||||
spec.label = std::move(label);
|
||||
spec.help = std::move(help);
|
||||
spec.defaultValue = defaultValue ? 1.0 : 0.0;
|
||||
return spec;
|
||||
}
|
||||
|
||||
ParamSpec ParamSpec::makeChoice(std::string key, std::string label, std::string defaultChoice,
|
||||
std::vector<std::string> choices, std::string help) {
|
||||
ParamSpec spec;
|
||||
spec.type = Type::Choice;
|
||||
spec.key = std::move(key);
|
||||
spec.label = std::move(label);
|
||||
spec.help = std::move(help);
|
||||
spec.defaultChoice = std::move(defaultChoice);
|
||||
spec.choices = std::move(choices);
|
||||
return spec;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
const ParamValue* find(const ParamValues& values, const std::string& key) {
|
||||
const auto it = values.find(key);
|
||||
return it == values.end() ? nullptr : &it->second;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
long paramInt(const ParamValues& values, const std::string& key, long fallback) {
|
||||
if (const auto* value = find(values, key)) {
|
||||
if (const auto* v = std::get_if<long>(value)) return *v;
|
||||
if (const auto* v = std::get_if<double>(value)) return static_cast<long>(*v);
|
||||
if (const auto* v = std::get_if<bool>(value)) return *v ? 1 : 0;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
double paramDouble(const ParamValues& values, const std::string& key, double fallback) {
|
||||
if (const auto* value = find(values, key)) {
|
||||
if (const auto* v = std::get_if<double>(value)) return *v;
|
||||
if (const auto* v = std::get_if<long>(value)) return static_cast<double>(*v);
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
bool paramBool(const ParamValues& values, const std::string& key, bool fallback) {
|
||||
if (const auto* value = find(values, key)) {
|
||||
if (const auto* v = std::get_if<bool>(value)) return *v;
|
||||
if (const auto* v = std::get_if<long>(value)) return *v != 0;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
std::string paramChoice(const ParamValues& values, const std::string& key,
|
||||
const std::string& fallback) {
|
||||
if (const auto* value = find(values, key)) {
|
||||
if (const auto* v = std::get_if<std::string>(value)) return *v;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
} // namespace effector
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace effector {
|
||||
|
||||
// Declarative description of one effect knob. The GUI turns these into widgets
|
||||
// and the CLI turns them into flags, so neither knows about any effect.
|
||||
struct ParamSpec {
|
||||
enum class Type { Int, Double, Bool, Choice };
|
||||
|
||||
Type type = Type::Int;
|
||||
std::string key;
|
||||
std::string label;
|
||||
std::string help;
|
||||
double defaultValue = 0.0;
|
||||
double minimum = 0.0;
|
||||
double maximum = 0.0;
|
||||
double step = 1.0;
|
||||
int digits = 2;
|
||||
std::string defaultChoice;
|
||||
std::vector<std::string> choices;
|
||||
|
||||
std::string flag() const; // "strip_height" -> "--strip-height"
|
||||
|
||||
static ParamSpec makeInt(std::string key, std::string label, long defaultValue, long minimum,
|
||||
long maximum, std::string help = {}, long step = 1);
|
||||
static ParamSpec makeDouble(std::string key, std::string label, double defaultValue,
|
||||
double minimum, double maximum, std::string help = {},
|
||||
double step = 0.05, int digits = 2);
|
||||
static ParamSpec makeBool(std::string key, std::string label, bool defaultValue,
|
||||
std::string help = {});
|
||||
static ParamSpec makeChoice(std::string key, std::string label, std::string defaultChoice,
|
||||
std::vector<std::string> choices, std::string help = {});
|
||||
};
|
||||
|
||||
using ParamValue = std::variant<long, double, bool, std::string>;
|
||||
using ParamValues = std::map<std::string, ParamValue>;
|
||||
|
||||
long paramInt(const ParamValues& values, const std::string& key, long fallback);
|
||||
double paramDouble(const ParamValues& values, const std::string& key, double fallback);
|
||||
bool paramBool(const ParamValues& values, const std::string& key, bool fallback);
|
||||
std::string paramChoice(const ParamValues& values, const std::string& key,
|
||||
const std::string& fallback);
|
||||
|
||||
} // namespace effector
|
||||
@@ -0,0 +1,29 @@
|
||||
#include "core/registry.hpp"
|
||||
|
||||
#include "effects/scanlines.hpp"
|
||||
|
||||
namespace effector {
|
||||
|
||||
// To add an effect: include its header and add one line here.
|
||||
const std::vector<EffectPtr>& effects() {
|
||||
static const std::vector<EffectPtr> registry = {
|
||||
std::make_shared<ScanlinesEffect>(),
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
EffectPtr effectByName(const std::string& name) {
|
||||
for (const auto& effect : effects()) {
|
||||
if (effect->name() == name) return effect;
|
||||
}
|
||||
std::string known;
|
||||
for (const auto& effect : effects()) {
|
||||
if (!known.empty()) known += ", ";
|
||||
known += effect->name();
|
||||
}
|
||||
throw EffectError("Unknown effect '" + name + "' (available: " + known + ")");
|
||||
}
|
||||
|
||||
std::string defaultEffectName() { return effects().front()->name(); }
|
||||
|
||||
} // namespace effector
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/effect.hpp"
|
||||
|
||||
namespace effector {
|
||||
|
||||
// Every effect the app knows about, in menu order.
|
||||
const std::vector<EffectPtr>& effects();
|
||||
|
||||
// Throws EffectError when the name is unknown.
|
||||
EffectPtr effectByName(const std::string& name);
|
||||
|
||||
std::string defaultEffectName();
|
||||
|
||||
} // namespace effector
|
||||
@@ -0,0 +1,12 @@
|
||||
#include "core/rng.hpp"
|
||||
|
||||
#include <random>
|
||||
|
||||
namespace effector {
|
||||
|
||||
std::uint64_t Rng::randomSeed() {
|
||||
std::random_device rd;
|
||||
return (static_cast<std::uint64_t>(rd()) << 32) ^ rd();
|
||||
}
|
||||
|
||||
} // namespace effector
|
||||
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace effector {
|
||||
|
||||
// xoshiro256** seeded through splitmix64. Rolled by hand rather than taken from
|
||||
// <random> so that a given seed produces the same image on every platform and
|
||||
// standard library.
|
||||
class Rng {
|
||||
public:
|
||||
explicit Rng(std::uint64_t seed) noexcept {
|
||||
for (auto& s : state_) s = splitmix(seed);
|
||||
}
|
||||
|
||||
std::uint64_t next() noexcept {
|
||||
const std::uint64_t result = rotl(state_[1] * 5, 7) * 9;
|
||||
const std::uint64_t t = state_[1] << 17;
|
||||
state_[2] ^= state_[0];
|
||||
state_[3] ^= state_[1];
|
||||
state_[1] ^= state_[2];
|
||||
state_[0] ^= state_[3];
|
||||
state_[2] ^= t;
|
||||
state_[3] = rotl(state_[3], 45);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Uniform in [0, bound] inclusive, unbiased (Lemire with rejection).
|
||||
std::uint64_t below(std::uint64_t bound) noexcept {
|
||||
if (bound == 0) return 0;
|
||||
const std::uint64_t range = bound + 1;
|
||||
const std::uint64_t limit = range == 0 ? 0 : (~std::uint64_t{0} / range) * range;
|
||||
std::uint64_t value;
|
||||
do {
|
||||
value = next();
|
||||
} while (limit != 0 && value >= limit);
|
||||
return value % range;
|
||||
}
|
||||
|
||||
// Uniform in [lo, hi] inclusive.
|
||||
long inRange(long lo, long hi) noexcept {
|
||||
if (hi <= lo) return lo;
|
||||
return lo + static_cast<long>(below(static_cast<std::uint64_t>(hi - lo)));
|
||||
}
|
||||
|
||||
static std::uint64_t randomSeed();
|
||||
|
||||
private:
|
||||
static std::uint64_t rotl(std::uint64_t x, int k) noexcept {
|
||||
return (x << k) | (x >> (64 - k));
|
||||
}
|
||||
|
||||
static std::uint64_t splitmix(std::uint64_t& seed) noexcept {
|
||||
std::uint64_t z = (seed += 0x9e3779b97f4a7c15ULL);
|
||||
z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9ULL;
|
||||
z = (z ^ (z >> 27)) * 0x94d049bb133111ebULL;
|
||||
return z ^ (z >> 31);
|
||||
}
|
||||
|
||||
std::uint64_t state_[4]{};
|
||||
};
|
||||
|
||||
} // namespace effector
|
||||
Reference in New Issue
Block a user