mirror of
https://github.com/MorizzG/ray-tracer.git
synced 2025-12-06 04:22:42 +00:00
changed philox to rand
This commit is contained in:
parent
98f0414357
commit
fa90f444ef
17 changed files with 177 additions and 45 deletions
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -1,6 +1,3 @@
|
||||||
[submodule "subprojects/philox"]
|
|
||||||
path = subprojects/philox
|
|
||||||
url = git@github.com:MorizzG/philox
|
|
||||||
[submodule "subprojects/rand"]
|
[submodule "subprojects/rand"]
|
||||||
path = subprojects/rand
|
path = subprojects/rand
|
||||||
url = git@github.com:MorizzG/rand
|
url = git@github.com:MorizzG/rand
|
||||||
|
|
|
||||||
21
meson.build
21
meson.build
|
|
@ -12,9 +12,24 @@ add_project_arguments(
|
||||||
language : 'cpp'
|
language : 'cpp'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if get_option('native')
|
||||||
|
add_global_arguments(
|
||||||
|
'-march=native',
|
||||||
|
|
||||||
philox_proj = subproject('philox', default_options : ['warning_level=0', 'werror=false'])
|
language : 'c'
|
||||||
philox_dep = philox_proj.get_variable('philox_dep')
|
)
|
||||||
|
|
||||||
|
add_global_arguments(
|
||||||
|
'-march=native',
|
||||||
|
|
||||||
|
language : 'cpp'
|
||||||
|
)
|
||||||
|
endif
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
rand_proj = subproject('rand', default_options : ['warning_level=0', 'werror=false'])
|
||||||
|
rand_dep = rand_proj.get_variable('rand_dep')
|
||||||
|
|
||||||
|
|
||||||
sources = []
|
sources = []
|
||||||
|
|
@ -22,4 +37,4 @@ subdir('src')
|
||||||
|
|
||||||
inc = include_directories('src')
|
inc = include_directories('src')
|
||||||
|
|
||||||
executable('ray-tracer', sources, include_directories : inc, dependencies : [philox_dep])
|
executable('ray-tracer', sources, include_directories : inc, dependencies : [rand_dep])
|
||||||
|
|
|
||||||
1
meson.options
Normal file
1
meson.options
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
option('native', type : 'boolean', value : true)
|
||||||
BIN
perf.data
Normal file
BIN
perf.data
Normal file
Binary file not shown.
BIN
perf.data.old
Normal file
BIN
perf.data.old
Normal file
Binary file not shown.
13
src/colour.h
13
src/colour.h
|
|
@ -1,6 +1,7 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
|
#include <cmath>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <ios>
|
#include <ios>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
@ -43,6 +44,10 @@ class Colour {
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constexpr Colour to_gamma2() const {
|
||||||
|
return Colour{std::sqrt(r()), std::sqrt(g()), std::sqrt(b())};
|
||||||
|
}
|
||||||
|
|
||||||
std::string to_string() const {
|
std::string to_string() const {
|
||||||
std::stringstream ss;
|
std::stringstream ss;
|
||||||
|
|
||||||
|
|
@ -74,8 +79,12 @@ constexpr Colour operator+(Colour c1, Colour c2) {
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline std::ostream& operator<<(std::ostream& os, Colour colour) {
|
inline std::ostream& operator<<(std::ostream& os, Colour c) {
|
||||||
os << FToU8(colour.r()) << ' ' << FToU8(colour.g()) << ' ' << FToU8(colour.b());
|
double r = c.r();
|
||||||
|
double g = c.g();
|
||||||
|
double b = c.b();
|
||||||
|
|
||||||
|
os << FToU8(r) << ' ' << FToU8(g) << ' ' << FToU8(b);
|
||||||
|
|
||||||
return os;
|
return os;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,18 +52,15 @@ constexpr std::ostream& operator<<(std::ostream& os, const Image& img) {
|
||||||
os << "255" << newline;
|
os << "255" << newline;
|
||||||
|
|
||||||
for (u32 j = 0; j < img.height(); j++) {
|
for (u32 j = 0; j < img.height(); j++) {
|
||||||
std::clog << "\rWriting line " << j << " of " << img.height() << std::flush;
|
|
||||||
|
|
||||||
for (u32 i = 0; i < img.width(); i++) {
|
for (u32 i = 0; i < img.width(); i++) {
|
||||||
const Colour col = img[i, j];
|
const Colour col = img[i, j];
|
||||||
|
|
||||||
col.CheckValid();
|
col.CheckValid();
|
||||||
|
|
||||||
os << col << newline;
|
// apply gamma 2 transform
|
||||||
|
os << col.to_gamma2() << newline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::clog << "\rDone " << newline;
|
|
||||||
|
|
||||||
return os;
|
return os;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
11
src/main.cc
11
src/main.cc
|
|
@ -2,10 +2,11 @@
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include "camera.h"
|
#include "camera.h"
|
||||||
|
#include "colour.h"
|
||||||
#include "image.h"
|
#include "image.h"
|
||||||
|
#include "material.h"
|
||||||
#include "raytracer.h"
|
#include "raytracer.h"
|
||||||
#include "renderer.h"
|
#include "renderer.h"
|
||||||
#include "renderobject.h"
|
|
||||||
#include "renderobjectlist.h"
|
#include "renderobjectlist.h"
|
||||||
#include "sphere.h"
|
#include "sphere.h"
|
||||||
#include "vec3.h"
|
#include "vec3.h"
|
||||||
|
|
@ -15,14 +16,16 @@ int main(int /* argc */, char* /* argv */[]) {
|
||||||
|
|
||||||
RenderObjectList world;
|
RenderObjectList world;
|
||||||
|
|
||||||
world.Add(std::make_shared<Sphere>(Vec3{0, -100.5, -1}, 100));
|
auto lamb = std::make_shared<Lambertian>(Colour{0.1, 0.4, 0.8});
|
||||||
world.Add(std::make_shared<Sphere>(-Vec3::e_z, 0.5));
|
|
||||||
|
world.Add(std::make_unique<Sphere>(Vec3{0, -100.5, -1}, 100, lamb));
|
||||||
|
world.Add(std::make_unique<Sphere>(-Vec3::e_z, 0.5, lamb));
|
||||||
|
|
||||||
// camera
|
// camera
|
||||||
|
|
||||||
constexpr f64 aspect_ratio = 16.0 / 9.0;
|
constexpr f64 aspect_ratio = 16.0 / 9.0;
|
||||||
|
|
||||||
constexpr u32 image_width = 800;
|
constexpr u32 image_width = 600;
|
||||||
constexpr auto image_height = static_cast<u32>(image_width / aspect_ratio);
|
constexpr auto image_height = static_cast<u32>(image_width / aspect_ratio);
|
||||||
|
|
||||||
// constexpr u32 image_width = 1920;
|
// constexpr u32 image_width = 1920;
|
||||||
|
|
|
||||||
52
src/material.h
Normal file
52
src/material.h
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <tuple>
|
||||||
|
|
||||||
|
#include "colour.h"
|
||||||
|
#include "rand.h"
|
||||||
|
#include "ray.h"
|
||||||
|
#include "renderobject.h"
|
||||||
|
|
||||||
|
class Material {
|
||||||
|
public:
|
||||||
|
constexpr Material() = default;
|
||||||
|
|
||||||
|
constexpr Material(const Material&) = default;
|
||||||
|
Material& operator=(const Material&) = default;
|
||||||
|
|
||||||
|
constexpr Material(Material&&) = default;
|
||||||
|
Material& operator=(Material&&) = default;
|
||||||
|
|
||||||
|
virtual ~Material() = default;
|
||||||
|
|
||||||
|
RandomGen& rand() const { return rand_; }
|
||||||
|
|
||||||
|
virtual std::optional<std::tuple<Colour, Ray>> Scatter(const Ray& in,
|
||||||
|
const HitRecord& hit_record) const = 0;
|
||||||
|
|
||||||
|
private:
|
||||||
|
mutable RandomGen rand_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
class Lambertian : public Material {
|
||||||
|
public:
|
||||||
|
constexpr explicit Lambertian(Colour albedo) : albedo_{albedo} {}
|
||||||
|
|
||||||
|
std::optional<std::tuple<Colour, Ray>> Scatter(const Ray& /* in */,
|
||||||
|
const HitRecord& hit_record) const override {
|
||||||
|
auto scatter_dir = hit_record.normal + rand().GenOnUnitSphere();
|
||||||
|
|
||||||
|
// TODO: enable
|
||||||
|
/* if (scatter_dir.almost_zero()) {
|
||||||
|
scatter_dir = hit_record.normal;
|
||||||
|
} */
|
||||||
|
|
||||||
|
Ray scattered{hit_record.p, scatter_dir};
|
||||||
|
|
||||||
|
return std::make_tuple(albedo_, scattered);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Colour albedo_;
|
||||||
|
};
|
||||||
|
|
@ -10,6 +10,8 @@ using u64 = uint64_t;
|
||||||
using f64 = double;
|
using f64 = double;
|
||||||
|
|
||||||
constexpr f64 kInf = std::numeric_limits<f64>::infinity();
|
constexpr f64 kInf = std::numeric_limits<f64>::infinity();
|
||||||
|
constexpr f64 kNan = std::numeric_limits<f64>::signaling_NaN();
|
||||||
|
|
||||||
constexpr f64 kPi = std::numbers::pi_v<f64>;
|
constexpr f64 kPi = std::numbers::pi_v<f64>;
|
||||||
|
|
||||||
constexpr char newline = '\n';
|
constexpr char newline = '\n';
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cassert>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
#include "camera.h"
|
#include "camera.h"
|
||||||
#include "colour.h"
|
#include "colour.h"
|
||||||
#include "image.h"
|
#include "image.h"
|
||||||
#include "interval.h"
|
#include "interval.h"
|
||||||
|
#include "material.h"
|
||||||
#include "rand.h"
|
#include "rand.h"
|
||||||
#include "ray.h"
|
#include "ray.h"
|
||||||
#include "raytracer.h"
|
#include "raytracer.h"
|
||||||
|
|
@ -14,13 +18,22 @@
|
||||||
|
|
||||||
class Renderer {
|
class Renderer {
|
||||||
public:
|
public:
|
||||||
constexpr explicit Renderer(Camera camera, u32 samples_per_pixel = 10)
|
constexpr explicit Renderer(Camera camera, u32 samples_per_pixel = 100, u32 max_bounces = 50)
|
||||||
: camera_{camera}, samples_per_pixel_{samples_per_pixel} {}
|
: camera_{camera}, samples_per_pixel_{samples_per_pixel}, max_bounces_{max_bounces} {}
|
||||||
|
|
||||||
constexpr Image Render(const RenderObject& world) {
|
constexpr u32& samples_per_pixel() { return samples_per_pixel_; }
|
||||||
|
constexpr u32 samples_per_pixel() const { return samples_per_pixel_; }
|
||||||
|
|
||||||
|
constexpr u32& max_bounces() { return max_bounces_; }
|
||||||
|
constexpr u32 max_bounces() const { return max_bounces_; }
|
||||||
|
|
||||||
|
Image Render(const RenderObject& world) {
|
||||||
Image img{camera_.image_width(), camera_.image_height()};
|
Image img{camera_.image_width(), camera_.image_height()};
|
||||||
|
|
||||||
for (u32 j = 0; j < img.height(); j++) {
|
constexpr u32 kNumThreads = 4;
|
||||||
|
|
||||||
|
auto render_thread = [this, &world, &img](u32 thread) {
|
||||||
|
for (u32 j = thread; j < img.height(); j += kNumThreads) {
|
||||||
std::clog << "\rWriting line " << j << " of " << img.height() << std::flush;
|
std::clog << "\rWriting line " << j << " of " << img.height() << std::flush;
|
||||||
|
|
||||||
for (u32 i = 0; i < img.width(); i++) {
|
for (u32 i = 0; i < img.width(); i++) {
|
||||||
|
|
@ -29,12 +42,27 @@ class Renderer {
|
||||||
for (u32 num_sample = 0; num_sample < samples_per_pixel_; num_sample++) {
|
for (u32 num_sample = 0; num_sample < samples_per_pixel_; num_sample++) {
|
||||||
auto ray = SampleRay(i, j);
|
auto ray = SampleRay(i, j);
|
||||||
|
|
||||||
colour_sum += Cast(ray, world);
|
colour_sum += Cast(ray, world, max_bounces_);
|
||||||
}
|
}
|
||||||
|
|
||||||
img[i, j] = colour_sum / samples_per_pixel_;
|
img[i, j] = colour_sum / samples_per_pixel_;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// render_thread(0);
|
||||||
|
|
||||||
|
std::array<std::thread, kNumThreads> render_threads;
|
||||||
|
|
||||||
|
for (u32 i = 0; i < kNumThreads; i++) {
|
||||||
|
render_threads[i] = std::thread{render_thread, i};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& t : render_threads) {
|
||||||
|
t.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::clog << "\rDone " << newline;
|
||||||
|
|
||||||
return img;
|
return img;
|
||||||
}
|
}
|
||||||
|
|
@ -53,22 +81,29 @@ class Renderer {
|
||||||
return Ray{camera_.centre(), ray_direction};
|
return Ray{camera_.centre(), ray_direction};
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr Colour Cast(const Ray& ray, const RenderObject& world) {
|
constexpr Colour Cast(const Ray& ray, const RenderObject& world, u32 bounces) {
|
||||||
auto hit_record = world.hit(ray, Interval::kPositive);
|
auto hit_record = world.hit(ray, Interval{0.001, kInf});
|
||||||
|
|
||||||
if (hit_record.has_value()) {
|
if (hit_record.has_value()) {
|
||||||
Vec3 normal = hit_record->normal;
|
if (!hit_record->front_face || bounces == 0) {
|
||||||
|
|
||||||
if (!hit_record->front_face) {
|
|
||||||
return Colour::kBlack;
|
return Colour::kBlack;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Vec3 normal = hit_record->normal;
|
||||||
|
|
||||||
Vec3 new_origin = hit_record->p;
|
Vec3 new_origin = hit_record->p;
|
||||||
Vec3 new_direction = rand_.GenOnHemisphere(normal);
|
|
||||||
|
|
||||||
Ray new_ray{new_origin, new_direction};
|
// Vec3 new_direction = rand_.GenOnHemisphere(normal);
|
||||||
|
Vec3 new_direction = normal + rand_.GenOnUnitSphere();
|
||||||
|
|
||||||
return 0.5 * Cast(new_ray, world);
|
Ray new_ray{new_origin, new_direction}; */
|
||||||
|
|
||||||
|
auto res = hit_record->mat->Scatter(ray, hit_record.value());
|
||||||
|
|
||||||
|
auto [albedo, out_ray] = res.value();
|
||||||
|
|
||||||
|
assert(bounces > 0);
|
||||||
|
return 0.5 * Cast(out_ray, world, bounces - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto unit_dir = ray.direction().normed();
|
auto unit_dir = ray.direction().normed();
|
||||||
|
|
@ -83,4 +118,5 @@ class Renderer {
|
||||||
RandomGen rand_{};
|
RandomGen rand_{};
|
||||||
|
|
||||||
u32 samples_per_pixel_;
|
u32 samples_per_pixel_;
|
||||||
|
u32 max_bounces_;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,17 @@
|
||||||
|
|
||||||
#include "interval.h"
|
#include "interval.h"
|
||||||
#include "ray.h"
|
#include "ray.h"
|
||||||
|
#include "raytracer.h"
|
||||||
#include "vec3.h"
|
#include "vec3.h"
|
||||||
|
|
||||||
|
class Material;
|
||||||
|
|
||||||
struct HitRecord {
|
struct HitRecord {
|
||||||
Point3 p;
|
Point3 p;
|
||||||
Vec3 normal;
|
Vec3 normal;
|
||||||
f64 t = 0.0;
|
const Material* mat = nullptr;
|
||||||
|
|
||||||
|
f64 t = kNan;
|
||||||
bool front_face = true;
|
bool front_face = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,21 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <initializer_list>
|
#include <initializer_list>
|
||||||
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "interval.h"
|
#include "interval.h"
|
||||||
#include "ray.h"
|
#include "ray.h"
|
||||||
|
#include "raytracer.h"
|
||||||
#include "renderobject.h"
|
#include "renderobject.h"
|
||||||
|
|
||||||
class RenderObjectList : public RenderObject {
|
class RenderObjectList : public RenderObject {
|
||||||
public:
|
public:
|
||||||
constexpr RenderObjectList() = default;
|
constexpr RenderObjectList() = default;
|
||||||
// explicit RenderObjectList(const SharedRenderObject& obj) { Append(obj); }
|
// explicit RenderObjectList(const SharedRenderObject& obj) { Append(obj); }
|
||||||
RenderObjectList(std::initializer_list<SharedRenderObject> objs) : objs_{objs} {}
|
// RenderObjectList(std::initializer_list<RenderObject> objs) : objs_{objs} {}
|
||||||
|
|
||||||
constexpr auto begin() { return objs_.begin(); }
|
constexpr auto begin() { return objs_.begin(); }
|
||||||
constexpr auto end() { return objs_.end(); }
|
constexpr auto end() { return objs_.end(); }
|
||||||
|
|
@ -20,7 +23,7 @@ class RenderObjectList : public RenderObject {
|
||||||
constexpr auto begin() const { return objs_.begin(); }
|
constexpr auto begin() const { return objs_.begin(); }
|
||||||
constexpr auto end() const { return objs_.end(); }
|
constexpr auto end() const { return objs_.end(); }
|
||||||
|
|
||||||
void Add(const SharedRenderObject& obj) { objs_.push_back(obj); }
|
void Add(std::unique_ptr<RenderObject> obj) { objs_.emplace_back(std::move(obj)); }
|
||||||
|
|
||||||
void Clear() { objs_.clear(); }
|
void Clear() { objs_.clear(); }
|
||||||
|
|
||||||
|
|
@ -43,5 +46,5 @@ class RenderObjectList : public RenderObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::vector<SharedRenderObject> objs_;
|
std::vector<std::unique_ptr<RenderObject>> objs_;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
11
src/sphere.h
11
src/sphere.h
|
|
@ -2,9 +2,11 @@
|
||||||
|
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
||||||
#include "interval.h"
|
#include "interval.h"
|
||||||
|
#include "material.h"
|
||||||
#include "ray.h"
|
#include "ray.h"
|
||||||
#include "raytracer.h"
|
#include "raytracer.h"
|
||||||
#include "renderobject.h"
|
#include "renderobject.h"
|
||||||
|
|
@ -13,7 +15,9 @@
|
||||||
class Sphere : public RenderObject {
|
class Sphere : public RenderObject {
|
||||||
public:
|
public:
|
||||||
constexpr Sphere() = default;
|
constexpr Sphere() = default;
|
||||||
constexpr Sphere(Point3 centre, f64 radius) : centre_{centre}, radius_{radius} {
|
|
||||||
|
Sphere(Point3 centre, f64 radius, const std::shared_ptr<Material>& mat)
|
||||||
|
: centre_{centre}, radius_{radius}, mat_{mat} {
|
||||||
assert(radius >= 0);
|
assert(radius >= 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -21,7 +25,7 @@ class Sphere : public RenderObject {
|
||||||
constexpr f64 radius() const { return radius_; }
|
constexpr f64 radius() const { return radius_; }
|
||||||
|
|
||||||
std::optional<HitRecord> hit(const Ray& ray, Interval ts) const override {
|
std::optional<HitRecord> hit(const Ray& ray, Interval ts) const override {
|
||||||
HitRecord hit_record;
|
HitRecord hit_record{};
|
||||||
|
|
||||||
Vec3 oc = ray.origin() - centre_;
|
Vec3 oc = ray.origin() - centre_;
|
||||||
|
|
||||||
|
|
@ -66,6 +70,7 @@ class Sphere : public RenderObject {
|
||||||
|
|
||||||
hit_record.t = t;
|
hit_record.t = t;
|
||||||
hit_record.p = ray.At(t);
|
hit_record.p = ray.At(t);
|
||||||
|
hit_record.mat = mat_.get();
|
||||||
|
|
||||||
if (hit_record.front_face) {
|
if (hit_record.front_face) {
|
||||||
hit_record.normal = (hit_record.p - centre_) / radius_;
|
hit_record.normal = (hit_record.p - centre_) / radius_;
|
||||||
|
|
@ -79,4 +84,6 @@ class Sphere : public RenderObject {
|
||||||
private:
|
private:
|
||||||
Point3 centre_{};
|
Point3 centre_{};
|
||||||
f64 radius_ = 0.0;
|
f64 radius_ = 0.0;
|
||||||
|
|
||||||
|
std::shared_ptr<Material> mat_;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,12 @@ class Vec3 {
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constexpr bool almost_zero() const {
|
||||||
|
f64 eps = 1e-8;
|
||||||
|
|
||||||
|
return std::fabs(x()) < eps && std::fabs(y()) < eps && std::fabs(z()) < eps;
|
||||||
|
}
|
||||||
|
|
||||||
constexpr f64 squared() const { return x() * x() + y() * y() + z() * z(); }
|
constexpr f64 squared() const { return x() * x() + y() * y() + z() * z(); }
|
||||||
|
|
||||||
constexpr f64 norm() const { return std::sqrt(squared()); }
|
constexpr f64 norm() const { return std::sqrt(squared()); }
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit cf69284bd2d82b279c34fb1cf90543fd04b3a038
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 4d7680624857241aca166f58b957712b0583fa05
|
Subproject commit 8043050d3c871d241a1274be81bfaf1894f5edb2
|
||||||
Loading…
Add table
Add a link
Reference in a new issue