From 1d7ad86fb70504400b547270344309d22ba9b2e3 Mon Sep 17 00:00:00 2001 From: sneeker Date: Sun, 10 Jul 2022 04:57:31 +0000 Subject: [PATCH] feat(examples): add the C++ benchmark and three test targets A C++ consumer of the C archives, plus maths, scene and image tests driven by meowc test. --- examples/raytracer/src/bench.cpp | 64 +++++++++++++++++++ examples/raytracer/tests/image_test.c | 91 +++++++++++++++++++++++++++ examples/raytracer/tests/math_test.c | 62 ++++++++++++++++++ examples/raytracer/tests/scene_test.c | 50 +++++++++++++++ 4 files changed, 267 insertions(+) create mode 100644 examples/raytracer/src/bench.cpp create mode 100644 examples/raytracer/tests/image_test.c create mode 100644 examples/raytracer/tests/math_test.c create mode 100644 examples/raytracer/tests/scene_test.c diff --git a/examples/raytracer/src/bench.cpp b/examples/raytracer/src/bench.cpp new file mode 100644 index 0000000..7ea77aa --- /dev/null +++ b/examples/raytracer/src/bench.cpp @@ -0,0 +1,64 @@ +// Throughput harness. Renders the same scene at rising sample counts and +// reports how the cost scales, which is a C++ consumer of the C libraries. +#include "rt_scene.h" +#include "studio_scene.h" + +#include +#include +#include +#include +#include + +namespace { + +struct Row { + int samples; + double seconds; + double paths_per_second; +}; + +Row measure(int width, int height, int samples, int threads) { + std::vector pixels(static_cast(width) * height * 3); + + scene sc{studio_spheres, studio_sphere_count, studio_background}; + camera cam = camera_make(studio_look_from, studio_look_at, v3(0, 1, 0), studio_fov, + static_cast(width) / height); + + render_job job{&sc, &cam, pixels.data(), width, height, + samples, 8, 0, threads}; + + const auto start = std::chrono::steady_clock::now(); + render_run(&job); + const std::chrono::duration elapsed = std::chrono::steady_clock::now() - start; + + const double paths = static_cast(width) * height * samples; + return Row{samples, elapsed.count(), paths / elapsed.count()}; +} + +} // namespace + +int main(int argc, char **argv) { + int width = 240, height = 135, threads = 4; + if (argc > 1) threads = std::atoi(argv[1]); + + std::cout << " bench " << width << "x" << height << ", " << threads << " threads\n\n"; + std::cout << " " << std::left << std::setw(12) << "samples" << std::setw(14) << "seconds" + << "M paths/s\n"; + + std::vector rows; + for (int s : {1, 4, 16, 64}) rows.push_back(measure(width, height, s, threads)); + + std::cout << std::fixed; + for (const Row &r : rows) { + std::cout << " " << std::left << std::setw(12) << r.samples << std::setw(14) + << std::setprecision(3) << r.seconds << std::setprecision(2) + << r.paths_per_second / 1e6 << "\n"; + } + + // Scaling should be close to linear in sample count; report the drift. + const double first = rows.front().paths_per_second; + const double last = rows.back().paths_per_second; + std::cout << "\n throughput drift " << std::setprecision(1) << (last / first - 1.0) * 100.0 + << " %\n"; + return 0; +} diff --git a/examples/raytracer/tests/image_test.c b/examples/raytracer/tests/image_test.c new file mode 100644 index 0000000..d207644 --- /dev/null +++ b/examples/raytracer/tests/image_test.c @@ -0,0 +1,91 @@ +#include "rt_config.h" +#include "rt_image.h" +#include +#include +#include + +static int failures = 0; + +static void check(const char *what, int cond) { + if (!cond) { + fprintf(stderr, " %s: failed\n", what); + failures++; + } +} + +static long read_all(const char *path, unsigned char **out) { + FILE *f = fopen(path, "rb"); + if (!f) return -1; + fseek(f, 0, SEEK_END); + long n = ftell(f); + fseek(f, 0, SEEK_SET); + unsigned char *buf = malloc((size_t)n); + if (!buf || fread(buf, 1u, (size_t)n, f) != (size_t)n) { + free(buf); + fclose(f); + return -1; + } + fclose(f); + *out = buf; + return n; +} + +static unsigned long be32(const unsigned char *p) { + return ((unsigned long)p[0] << 24) | ((unsigned long)p[1] << 16) | ((unsigned long)p[2] << 8) | p[3]; +} + +int main(void) { + image *im = image_new(17, 11); + check("the image allocates", im != NULL); + if (!im) return 1; + + check("width is kept", im->width == 17); + check("the buffer starts black", im->rgb[0] == 0 && im->rgb[17 * 11 * 3 - 1] == 0); + + for (int y = 0; y < im->height; y++) + for (int x = 0; x < im->width; x++) { + unsigned char *px = im->rgb + ((size_t)y * 17 + (size_t)x) * 3; + px[0] = (unsigned char)(x * 15); + px[1] = (unsigned char)(y * 23); + px[2] = 128; + } + + check("a zero sized image is refused", image_new(0, 4) == NULL); + + const char *ppm = "test-out.ppm"; + check("ppm writes", image_write_ppm(im, ppm) == 0); + unsigned char *buf = NULL; + long n = read_all(ppm, &buf); + check("ppm has a header and every pixel", n == (long)(strlen("P6\n17 11\n255\n") + 17 * 11 * 3)); + check("ppm starts with the magic", n > 2 && buf[0] == 'P' && buf[1] == '6'); + free(buf); + remove(ppm); + +#ifdef HAVE_ZLIB + const char *png = "test-out.png"; + check("png writes", image_write_png(im, png) == 0); + n = read_all(png, &buf); + check("png is not empty", n > 8); + if (n > 8) { + static const unsigned char sig[8] = {137, 80, 78, 71, 13, 10, 26, 10}; + check("png signature", memcmp(buf, sig, 8) == 0); + check("first chunk is IHDR", memcmp(buf + 12, "IHDR", 4) == 0); + check("IHDR carries the width", be32(buf + 16) == 17); + check("IHDR carries the height", be32(buf + 20) == 11); + check("eight bits per channel", buf[24] == 8); + check("truecolour", buf[25] == 2); + check("last chunk is IEND", memcmp(buf + n - 8, "IEND", 4) == 0); + } + free(buf); + remove(png); + check("the default extension follows zlib", strcmp(image_default_extension(), "png") == 0); +#else + check("without zlib the default is ppm", strcmp(image_default_extension(), "ppm") == 0); +#endif + + image_free(im); + image_free(NULL); /* must tolerate a null pointer */ + + if (failures == 0) printf(" image: every assertion held\n"); + return failures == 0 ? 0 : 1; +} diff --git a/examples/raytracer/tests/math_test.c b/examples/raytracer/tests/math_test.c new file mode 100644 index 0000000..10bb22b --- /dev/null +++ b/examples/raytracer/tests/math_test.c @@ -0,0 +1,62 @@ +#include "rt.h" +#include +#include + +static int failures = 0; + +static void near(const char *what, double got, double want, double tol) { + if (fabs(got - want) > tol) { + fprintf(stderr, " %s: got %.9f, want %.9f\n", what, got, want); + failures++; + } +} + +static void check(const char *what, int cond) { + if (!cond) { + fprintf(stderr, " %s: failed\n", what); + failures++; + } +} + +int main(void) { + vec3 a = v3(1.0, 2.0, 3.0), b = v3(-4.0, 5.0, -6.0); + + near("dot", v3dot(a, b), -4.0 + 10.0 - 18.0, 1e-12); + near("len", v3len(v3(3.0, 4.0, 0.0)), 5.0, 1e-12); + near("norm is unit", v3len(v3norm(b)), 1.0, 1e-12); + + vec3 c = v3cross(a, b); + near("cross is perpendicular to a", v3dot(c, a), 0.0, 1e-12); + near("cross is perpendicular to b", v3dot(c, b), 0.0, 1e-12); + + /* A ray striking a surface head on comes straight back. */ + vec3 r = v3reflect(v3(0.0, -1.0, 0.0), v3(0.0, 1.0, 0.0)); + near("reflect x", r.x, 0.0, 1e-12); + near("reflect y", r.y, 1.0, 1e-12); + + vec3 mid = v3lerp(v3(0.0, 0.0, 0.0), v3(2.0, 4.0, 6.0), 0.5); + near("lerp midpoint", mid.y, 2.0, 1e-12); + + near("zero length normalises to itself", v3len(v3norm(v3(0, 0, 0))), 0.0, 1e-12); + + /* The generator must be reproducible from a seed, and stay in range. */ + rng x, y; + rng_seed(&x, 12345); + rng_seed(&y, 12345); + double sum = 0.0, lo = 1.0, hi = 0.0; + for (int i = 0; i < 200000; i++) { + double u = rng_unit(&x); + check("same seed gives the same stream", u == rng_unit(&y)); + if (u < lo) lo = u; + if (u > hi) hi = u; + sum += u; + } + check("stays inside [0,1)", lo >= 0.0 && hi < 1.0); + near("mean is near one half", sum / 200000.0, 0.5, 0.005); + + for (int i = 0; i < 5000; i++) check("unit vector is unit", fabs(v3len(rng_unit_vector(&x)) - 1.0) < 1e-9); + for (int i = 0; i < 5000; i++) check("sample sits inside the sphere", v3len2(rng_in_sphere(&x)) < 1.0); + + if (failures == 0) printf(" math: every assertion held\n"); + return failures == 0 ? 0 : 1; +} diff --git a/examples/raytracer/tests/scene_test.c b/examples/raytracer/tests/scene_test.c new file mode 100644 index 0000000..3a06cf5 --- /dev/null +++ b/examples/raytracer/tests/scene_test.c @@ -0,0 +1,50 @@ +#include "rt_scene.h" +#include "studio_scene.h" +#include +#include + +static int failures = 0; + +static void check(const char *what, int cond) { + if (!cond) { + fprintf(stderr, " %s: failed\n", what); + failures++; + } +} + +int main(void) { + check("the generated scene has spheres", studio_sphere_count > 0); + + /* One unit sphere at the origin, hit straight down the -z axis. */ + static const sphere one[] = {{{0, 0, 0}, 1.0, {MAT_LAMBERT, {0.5, 0.5, 0.5}, 0.0}}}; + scene sc = {one, 1, {0, 0, 0}}; + + ray r = {{0, 0, 5}, {0, 0, -1}}; + hit h; + check("a ray aimed at the sphere hits it", scene_hit(&sc, r, 1e-4, 1e30, &h)); + check("it hits the near face", fabs(h.t - 4.0) < 1e-9); + check("the normal faces the ray", fabs(h.normal.z - 1.0) < 1e-9); + check("the hit is on the outside", h.front == 1); + + ray miss = {{0, 5, 5}, {0, 0, -1}}; + check("a ray that passes above misses", !scene_hit(&sc, miss, 1e-4, 1e30, &h)); + + /* Starting inside, the first crossing is the far wall. */ + ray inside = {{0, 0, 0}, {0, 0, -1}}; + check("a ray from the centre still hits", scene_hit(&sc, inside, 1e-4, 1e30, &h)); + check("and reports the inside face", h.front == 0); + + /* The interval bounds are respected. */ + check("a hit beyond tmax is rejected", !scene_hit(&sc, r, 1e-4, 1.0, &h)); + + /* Nearest sphere wins when two overlap the ray. */ + static const sphere two[] = { + {{0, 0, 0}, 1.0, {MAT_LAMBERT, {1, 0, 0}, 0.0}}, + {{0, 0, 2.5}, 0.5, {MAT_METAL, {0, 1, 0}, 0.0}}, + }; + scene both = {two, 2, {0, 0, 0}}; + check("the closer sphere is chosen", scene_hit(&both, r, 1e-4, 1e30, &h) && h.mat.kind == MAT_METAL); + + if (failures == 0) printf(" scene: every assertion held\n"); + return failures == 0 ? 0 : 1; +}