Merge branch 'master' into 'feature/kos-texture-stride'

# Conflicts:
#   tests/test_glteximage2d.h
This commit is contained in:
Luke Benstead 2026-06-23 05:25:48 +00:00
commit cbc83deee0
23 changed files with 2028 additions and 85 deletions

View File

@ -16,6 +16,12 @@
#include "../containers/aligned_vector.h"
#include "../containers/named_array.h"
#ifdef __cplusplus
/* Ensure the helper functions declared below keep C linkage when this header
* is pulled into the C++ test harness, so they link against the C library. */
extern "C" {
#endif
#define MAX_GLDC_4BPP_PALETTE_SLOTS 16
#define MAX_GLDC_PALETTE_SLOTS 4
#define MAX_GLDC_SHARED_PALETTES (MAX_GLDC_PALETTE_SLOTS*MAX_GLDC_4BPP_PALETTE_SLOTS)
@ -480,4 +486,8 @@ float _glUnpackHalfFloat(half_float_t h);
#define MAX(a,b) (((a)>(b))?(a):(b))
#define CLAMP( X, _MIN, _MAX ) ( (X)<(_MIN) ? (_MIN) : ((X)>(_MAX) ? (_MAX) : (X)) )
#ifdef __cplusplus
}
#endif
#endif // PRIVATE_H

View File

@ -2,6 +2,15 @@
FILE(GLOB GL_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/test_*.h)
# The allocator tests are host-side unit tests for the standalone block
# allocator: they assert exact heap addresses and allocate multi-megabyte
# scratch pools, neither of which is meaningful (or safe) under KOS. Exclude
# them from the Dreamcast build; the allocator is still exercised on hardware
# indirectly through the texture tests.
if(PLATFORM_DREAMCAST)
list(REMOVE_ITEM GL_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/test_allocator.h)
endif()
INCLUDE_DIRECTORIES(${CMAKE_SOURCE_DIR})
SET(TEST_GENERATOR_BIN ${CMAKE_SOURCE_DIR}/tools/test_generator.py)
@ -16,6 +25,32 @@ ADD_CUSTOM_COMMAND(
add_executable(gldc_tests ${TEST_FILES} ${TEST_SOURCES} ${TEST_MAIN_FILENAME})
target_link_libraries(gldc_tests GL)
# Directory holding the committed golden reference images.
#
# On the desktop the tests read/write them straight from the source tree. On
# the Dreamcast the goldens are read through the dcload host-filesystem mount
# (KOS mounts it at /pc), so we point at /pc/goldens and copy the references
# next to the .elf so that running the emulator with `-C .` from the build
# directory exposes them as /pc/goldens.
if(PLATFORM_DREAMCAST)
set(GLDC_GOLDEN_DIR "/pc/goldens")
add_custom_command(
TARGET gldc_tests POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_CURRENT_SOURCE_DIR}/goldens
${CMAKE_CURRENT_BINARY_DIR}/goldens
COMMENT "Copying golden reference images next to gldc_tests.elf"
)
else()
set(GLDC_GOLDEN_DIR "${CMAKE_CURRENT_SOURCE_DIR}/goldens")
endif()
target_compile_definitions(
gldc_tests PRIVATE
GLDC_GOLDEN_DIR="${GLDC_GOLDEN_DIR}"
)
if(NOT PLATFORM_DREAMCAST)
set_target_properties(
gldc_tests

127
tests/README.md Normal file
View File

@ -0,0 +1,127 @@
# GLdc test suite
The tests are plain C++ headers under `tests/`. Every class deriving from
`test::TestCase` (or `GLTestCase`) is auto-discovered (see
`tools/test_generator.py`) and each `void test_*()` method becomes a test case.
They build into the `gldc_tests` binary.
The same tests run on two backends:
* **desktop / software** backend — fast iteration in CI;
* **Dreamcast / kospvr** backend — the real target, run under an emulator.
Suites that drive the GL API derive from **`GLTestCase`** (`tools/gl_test.h`),
which initialises the GPU exactly once and resets GL state before each test
(see "Running once on the Dreamcast" below). Pure non-GL suites (e.g. the
standalone allocator tests) derive from `test::TestCase` directly.
## Desktop
```sh
# configure + build (32-bit desktop build)
mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Debug ..
make gldc_tests
# run everything (headless)
SDL_VIDEODRIVER=dummy ./tests/gldc_tests
# run a single suite (prefix match on "Suite::test")
SDL_VIDEODRIVER=dummy ./tests/gldc_tests TextureFormatTests
```
## Dreamcast
Build with the KallistiOS toolchain inside the `kazade/dreamcast-sdk`
container, then run the resulting `.elf` under the
[nitrocast](https://gitlab.com/simulant/nitrocast) emulator:
```sh
# build (from the repo root; KOS_BASE is set inside the container)
podman run --rm -v "$PWD":"$PWD":Z -w "$PWD" localhost/kazade/dreamcast-sdk \
/bin/sh -c "source /etc/bash.bashrc; \
mkdir -p dcbuild && cd dcbuild && \
cmake -DCMAKE_TOOLCHAIN_FILE=../toolchains/Dreamcast.cmake \
-DCMAKE_BUILD_TYPE=Release -DBUILD_SAMPLES=OFF .. && \
make gldc_tests"
# run on the emulator. -u enables dcload host-file syscalls and -C mounts the
# given directory as the dcload filesystem (KOS sees it as /pc), which is where
# the golden references are read from. The build copies goldens/ next to the
# .elf, so run from there:
cd dcbuild/tests
nitrocast -b -e gldc_tests.elf -u -C . -r 32
```
The allocator unit tests are desktop-only (they assert exact host heap
addresses); they are automatically excluded from the Dreamcast build.
### Running once on the Dreamcast
`InitGPU()`/`ShutdownGPU()` map to `pvr_init()`/`pvr_shutdown()`, which must not
be torn down and brought back up between tests. `GLTestCase` therefore opens the
PVR (and the display) lazily on the first test, registers an `atexit()` handler
to shut it down once at program exit, and only *resets* GL state between tests.
New GL-driving suites should derive from `GLTestCase`; if they need extra setup
they should override `set_up()` and call `GLTestCase::set_up()` first.
## What's covered
| File | Focus |
|------|-------|
| `test_glcolor.h` | `glColor*` / `glColorPointer` state |
| `test_glteximage2d.h` | basic internal-format selection |
| `test_pvr_vertex_submission.h`| TA poly-list structure & headers |
| `test_vertex_formats.h` | `glVertexPointer` types/sizes/strides, immediate mode, `glDrawElements` |
| `test_texcoord_formats.h` | `glTexCoordPointer` type scaling, immediate `glTexCoord` |
| `test_texture_formats.h` | byte-exact texture conversion (RGB565 / ARGB4444 / ARGB1555 / RGBA8 / RED / ALPHA / paletted), `glTexSubImage2D`, errors |
| `test_golden_rendering.h` | end-to-end rendered-output comparison |
The format/submission tests work by inspecting the internal state the driver
produces — the converted texture bytes (`TextureObject::data`) and the submitted
vertices in `OP_LIST` / `PT_LIST` / `TR_LIST` — so they are exact and
deterministic, and they pin down the per-type readers and pixel conversions
without needing a framebuffer.
## Golden-image tests
`tools/golden.h` contains a small, self-contained, deterministic CPU rasteriser.
It consumes the **same** TA poly-lists the real backend submits and reproduces
the backend's triangle-strip walk and perspective divide, then fills triangles
with Gouraud vertex colour (optionally modulated by a decoded texture). Output is
compared against committed PPM references in `tests/goldens/`.
Because the rasteriser lives in the test harness (not the library) it doesn't
move when the library changes, so a diff in the rendered output reliably points
at a regression in GLdc's transform / colour / clipping / submission pipeline.
The poly-lists are produced by the real (SH4-compiled) library, so the same
references are validated against actual Dreamcast output too — on the Dreamcast
the textured cases even sample the decoded texture straight out of PVR VRAM.
Comparison is tolerant (per-channel + mismatch-fraction thresholds) so sub-LSB
rounding never causes flakiness.
The references live in `tests/goldens/`. On the desktop they are read straight
from the source tree; on the Dreamcast they are read through the dcload mount at
`/pc/goldens` (the build copies them next to the `.elf`, so `-C .` from the
build directory exposes them).
### Adding or updating a golden
1. Write a test that draws something and calls
`golden::rasterize_all_lists(img)` then
`assert_true(golden::check(img, "my_scene"))`.
2. Generate (or refresh) the reference image (desktop):
```sh
GLDC_UPDATE_GOLDENS=1 SDL_VIDEODRIVER=dummy ./tests/gldc_tests GoldenRenderingTests
```
3. **Eyeball the new `tests/goldens/my_scene.ppm`** before committing it
(any image viewer / `convert ... png` works) and commit it alongside the test.
On a mismatch the harness writes `my_scene.actual.ppm` and `my_scene.diff.ppm`
(differing pixels highlighted red) next to the golden for inspection.
> Note: the video mode is 640×480 and the viewport y-flip is relative to that
> height, so golden scenes place the viewport at `(0, 480 - H)` to capture a
> W×H image at the top-left of the framebuffer.

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,6 +1,7 @@
#pragma once
#include "tools/test.h"
#include "tools/gl_test.h"
#include <stdint.h>
#include <GL/gl.h>
@ -21,21 +22,11 @@
* index 2 = green (G8IDX)
* index 3 = blue (B8IDX)
* =========================================================================*/
class GlColorTests : public test::TestCase {
class GlColorTests : public GLTestCase {
public:
void set_up() {
GLdcConfig config;
glKosInitConfig(&config);
config.texture_twiddle = false;
glKosInitEx(&config);
/* _glInitContext does not reset current_color, so do it explicitly
* here so every test starts from a known white default. */
glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
}
void tear_down() {
glKosShutdown();
}
/* GLTestCase::set_up() initialises the GPU once and resets the current
* colour to white, which is the known-good starting state these tests
* expect. */
/* After set_up the current colour must be white (1,1,1,1). */
void test_default_color_is_white() {
@ -211,20 +202,9 @@ public:
* pointer, that stride=0 triggers the automatic stride calculation, and
* that invalid size arguments raise GL_INVALID_VALUE.
* =========================================================================*/
class GlColorPointerTests : public test::TestCase {
class GlColorPointerTests : public GLTestCase {
public:
void set_up() {
GLdcConfig config;
glKosInitConfig(&config);
config.texture_twiddle = false;
glKosInitEx(&config);
/* Reset enabled flags _glInitAttributePointers does not do this. */
ATTRIB_LIST.enabled = 0;
}
void tear_down() {
glKosShutdown();
}
/* GLTestCase::set_up() already clears ATTRIB_LIST.enabled. */
/* size=4, GL_FLOAT: stride auto-calculated to 4*4=16. */
void test_colorpointer_size4_float_stores_metadata() {
@ -352,19 +332,8 @@ public:
* Mirrors GlColorPointerTests for the secondary (offset) colour pointer
* stored in ATTRIB_LIST.s_color.
* =========================================================================*/
class GlSecondaryColorPointerTests : public test::TestCase {
class GlSecondaryColorPointerTests : public GLTestCase {
public:
void set_up() {
GLdcConfig config;
glKosInitConfig(&config);
config.texture_twiddle = false;
glKosInitEx(&config);
ATTRIB_LIST.enabled = 0;
}
void tear_down() {
glKosShutdown();
}
/* size=4, GL_FLOAT: basic happy path. */
void test_secondary_colorpointer_size4_float_stores_metadata() {
@ -460,20 +429,8 @@ public:
* appropriate flag bits in ATTRIB_LIST.enabled are set or cleared, that the
* dirty flag is updated, and that invalid enumerants raise GL_INVALID_ENUM.
* =========================================================================*/
class GlClientStateTests : public test::TestCase {
class GlClientStateTests : public GLTestCase {
public:
void set_up() {
GLdcConfig config;
glKosInitConfig(&config);
config.texture_twiddle = false;
glKosInitEx(&config);
/* Clear all enabled flags for a clean baseline. */
ATTRIB_LIST.enabled = 0;
}
void tear_down() {
glKosShutdown();
}
/* Enabling GL_COLOR_ARRAY must set COLOR_ENABLED_FLAG. */
void test_enable_color_array_sets_flag() {

View File

@ -1,4 +1,7 @@
#pragma once
#include "tools/test.h"
#include "tools/gl_test.h"
#include <cstring>
#include <stdint.h>
@ -7,17 +10,14 @@
#include <GL/glkos.h>
class TexImage2DTests : public test::TestCase {
class TexImage2DTests : public GLTestCase {
public:
uint8_t image_data[8 * 8 * 4] = {0};
uint8_t stride_image_data[96 * 48 * 4] = {0};
uint8_t unpack_row_image_data[128 * 48 * 4] = {0};
void set_up() {
GLdcConfig config;
glKosInitConfig(&config);
config.texture_twiddle = false;
glKosInitEx(&config);
GLTestCase::set_up();
/* Init image data so each texel RGBA value matches the
* position in the array */
@ -29,17 +29,6 @@ public:
}
}
void tear_down() {
glKosShutdown();
}
void set_clamp_wrap() {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
assert_equal(glGetError(), GL_NO_ERROR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
assert_equal(glGetError(), GL_NO_ERROR);
}
void test_rgb_to_rgb565() {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 8, 8, 0, GL_RGB, GL_UNSIGNED_BYTE, image_data);
assert_equal(glGetError(), GL_NO_ERROR);

View File

@ -0,0 +1,243 @@
#pragma once
#include "tools/test.h"
#include "tools/gl_test.h"
#include "tools/golden.h"
#include <stdint.h>
#include <GL/gl.h>
#include <GL/glkos.h>
#include "GL/private.h"
/* =========================================================================
* GoldenRenderingTests
*
* End-to-end "golden image" tests. Each test issues real GL draw calls, then
* rasterises the resulting TA poly-lists with the deterministic CPU rasteriser
* in tools/golden.h and compares the output to a committed PPM reference under
* tests/goldens/.
*
* These guard the whole submission pipeline vertex transform, viewport
* mapping, colour/UV handling, primitive assembly, list routing (opaque vs
* transparent) and texture sampling against regressions.
*
* To (re)generate the references run the test binary with GLDC_UPDATE_GOLDENS=1.
* =========================================================================*/
class GoldenRenderingTests : public GLTestCase {
public:
/* The video mode is 640x480 and the viewport y-flip is relative to that
* height, so to capture a WxH image at the top-left of the framebuffer we
* place the viewport at (0, 480 - H). */
static const int VIDEO_H = 480;
static const int W = 96;
static const int H = 96;
void set_up() {
GLTestCase::set_up();
glViewport(0, VIDEO_H - H, W, H);
}
/* A single white triangle on a black background. */
void test_solid_white_triangle() {
golden::Image img(W, H);
img.clear(0, 0, 0);
glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
glBegin(GL_TRIANGLES);
glVertex3f(-0.8f, -0.8f, 0.0f);
glVertex3f( 0.8f, -0.8f, 0.0f);
glVertex3f( 0.0f, 0.8f, 0.0f);
glEnd();
golden::rasterize_all_lists(img);
assert_true(golden::check(img, "solid_white_triangle"));
}
/* Gouraud-shaded triangle: red/green/blue corners interpolated. */
void test_gouraud_triangle() {
golden::Image img(W, H);
img.clear(0, 0, 0);
glBegin(GL_TRIANGLES);
glColor4f(1.0f, 0.0f, 0.0f, 1.0f); glVertex3f(-0.8f, -0.8f, 0.0f);
glColor4f(0.0f, 1.0f, 0.0f, 1.0f); glVertex3f( 0.8f, -0.8f, 0.0f);
glColor4f(0.0f, 0.0f, 1.0f, 1.0f); glVertex3f( 0.0f, 0.8f, 0.0f);
glEnd();
golden::rasterize_all_lists(img);
assert_true(golden::check(img, "gouraud_triangle"));
}
/* A full-screen-ish coloured quad built from a triangle strip. */
void test_colored_quad_strip() {
golden::Image img(W, H);
img.clear(16, 16, 16);
glBegin(GL_TRIANGLE_STRIP);
glColor4f(1.0f, 0.0f, 0.0f, 1.0f); glVertex3f(-0.7f, 0.7f, 0.0f);
glColor4f(1.0f, 1.0f, 0.0f, 1.0f); glVertex3f(-0.7f, -0.7f, 0.0f);
glColor4f(0.0f, 1.0f, 0.0f, 1.0f); glVertex3f( 0.7f, 0.7f, 0.0f);
glColor4f(0.0f, 0.0f, 1.0f, 1.0f); glVertex3f( 0.7f, -0.7f, 0.0f);
glEnd();
golden::rasterize_all_lists(img);
assert_true(golden::check(img, "colored_quad_strip"));
}
/* Two overlapping opaque triangles: painter's order must put the second
* (green) on top of the first (red). */
void test_overlapping_opaque_painter_order() {
golden::Image img(W, H);
img.clear(0, 0, 0);
glColor4f(1.0f, 0.0f, 0.0f, 1.0f);
glBegin(GL_TRIANGLES);
glVertex3f(-0.8f, -0.6f, 0.0f);
glVertex3f( 0.4f, -0.6f, 0.0f);
glVertex3f(-0.2f, 0.8f, 0.0f);
glEnd();
glColor4f(0.0f, 1.0f, 0.0f, 1.0f);
glBegin(GL_TRIANGLES);
glVertex3f(-0.4f, -0.8f, 0.0f);
glVertex3f( 0.8f, -0.8f, 0.0f);
glVertex3f( 0.2f, 0.6f, 0.0f);
glEnd();
golden::rasterize_all_lists(img);
assert_true(golden::check(img, "overlapping_opaque"));
}
/* A half-transparent quad over an opaque red triangle: routed to the
* transparent list and alpha-blended by the rasteriser. */
void test_transparent_over_opaque() {
golden::Image img(W, H);
img.clear(0, 0, 0);
/* Opaque red triangle (OP_LIST). */
glDisable(GL_BLEND);
glColor4f(1.0f, 0.0f, 0.0f, 1.0f);
glBegin(GL_TRIANGLES);
glVertex3f(-0.8f, -0.8f, 0.0f);
glVertex3f( 0.8f, -0.8f, 0.0f);
glVertex3f( 0.0f, 0.8f, 0.0f);
glEnd();
/* 50% blue quad on top (TR_LIST). */
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glColor4f(0.0f, 0.0f, 1.0f, 0.5f);
glBegin(GL_TRIANGLE_STRIP);
glVertex3f(-0.5f, 0.5f, 0.0f);
glVertex3f(-0.5f, -0.5f, 0.0f);
glVertex3f( 0.5f, 0.5f, 0.0f);
glVertex3f( 0.5f, -0.5f, 0.0f);
glEnd();
golden::rasterize_all_lists(img);
assert_true(golden::check(img, "transparent_over_opaque"));
}
/* A Gouraud quad coloured via a GL_BGRA GL_UNSIGNED_BYTE array (OpenGL 1.4),
* blended over a coloured background. This exercises the whole BGRA path end
* to end: the red/blue channel swap must land the right colour at each
* corner, and the per-vertex alpha must drive the blend against the
* background. If BGRA decoded as RGBA, or alpha were ignored, the rendered
* output would differ from the golden. */
void test_bgra_vertex_colors_with_alpha() {
golden::Image img(W, H);
img.clear(40, 40, 40); /* dark grey so blending is visible */
GLfloat verts[] = {
-0.8f, 0.8f, 0.0f,
-0.8f, -0.8f, 0.0f,
0.8f, 0.8f, 0.0f,
0.8f, -0.8f, 0.0f,
};
/* GL_BGRA memory order is B, G, R, A -> colour R, G, B, A.
* Distinct corners with decreasing alpha down/right. */
GLubyte colors[] = {
/* B G R A -> colour */
0, 0, 255, 255, /* opaque red */
0, 255, 0, 192, /* green, ~75% alpha */
255, 0, 0, 128, /* blue, 50% alpha */
0, 255, 255, 64, /* yellow, 25% alpha */
};
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
glVertexPointer(3, GL_FLOAT, 0, verts);
glColorPointer(GL_BGRA, GL_UNSIGNED_BYTE, 0, colors);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glDisableClientState(GL_COLOR_ARRAY);
glDisableClientState(GL_VERTEX_ARRAY);
golden::rasterize_all_lists(img);
assert_true(golden::check(img, "bgra_vertex_colors_alpha"));
}
/* A textured quad. A 2x2 RGB texture (red/green/blue/white) is mapped over
* a quad; nearest filtering gives four solid colour cells. */
void test_textured_quad() {
golden::Image img(W, H);
img.clear(0, 0, 0);
/* 8x8 (the PVR minimum) split into four 4x4 colour quadrants:
* red / green / blue / white. */
GLubyte texels[8 * 8 * 3];
for(int y = 0; y < 8; ++y) {
for(int x = 0; x < 8; ++x) {
GLubyte r = 0, g = 0, b = 0;
if(y < 4 && x < 4) { r = 255; } /* top-left red */
else if(y < 4) { g = 255; } /* top-right green */
else if(x < 4) { b = 255; } /* bottom-left blue */
else { r = g = b = 255; } /* bottom-right white */
int i = (y * 8 + x) * 3;
texels[i + 0] = r; texels[i + 1] = g; texels[i + 2] = b;
}
}
GLuint tex = 0;
glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_2D, tex);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 8, 8, 0, GL_RGB, GL_UNSIGNED_BYTE, texels);
assert_equal(glGetError(), GL_NO_ERROR);
glEnable(GL_TEXTURE_2D);
glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
glBegin(GL_TRIANGLE_STRIP);
glTexCoord2f(0.0f, 0.0f); glVertex3f(-0.8f, 0.8f, 0.0f);
glTexCoord2f(0.0f, 1.0f); glVertex3f(-0.8f, -0.8f, 0.0f);
glTexCoord2f(1.0f, 0.0f); glVertex3f( 0.8f, 0.8f, 0.0f);
glTexCoord2f(1.0f, 1.0f); glVertex3f( 0.8f, -0.8f, 0.0f);
glEnd();
golden::rasterize_all_lists(img, _glGetBoundTexture());
assert_true(golden::check(img, "textured_quad"));
glDeleteTextures(1, &tex);
}
/* The same triangle drawn with a coloured background must leave the
* background untouched outside the triangle (clear-colour preservation). */
void test_triangle_preserves_background() {
golden::Image img(W, H);
img.clear(40, 80, 120);
glColor4f(1.0f, 1.0f, 0.0f, 1.0f);
glBegin(GL_TRIANGLES);
glVertex3f(-0.5f, -0.5f, 0.0f);
glVertex3f( 0.5f, -0.5f, 0.0f);
glVertex3f( 0.0f, 0.5f, 0.0f);
glEnd();
golden::rasterize_all_lists(img);
assert_true(golden::check(img, "triangle_on_background"));
}
};

View File

@ -1,6 +1,7 @@
#pragma once
#include "tools/test.h"
#include "tools/gl_test.h"
#include <stdint.h>
#include <GL/gl.h>
@ -32,22 +33,10 @@
/* =========================================================================
* PVRVertexSubmissionTests
* =========================================================================*/
class PVRVertexSubmissionTests : public test::TestCase {
class PVRVertexSubmissionTests : public GLTestCase {
public:
void set_up() {
GLdcConfig config;
glKosInitConfig(&config);
config.texture_twiddle = false;
glKosInitEx(&config);
/* Explicit clean slate for colour and client-state. */
glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
ATTRIB_LIST.enabled = 0;
}
void tear_down() {
glKosShutdown();
}
/* GLTestCase::set_up() gives a clean slate: GPU initialised once, colour
* reset to white and all client-state arrays disabled. */
/* Convenience: cast element i of a list to Vertex*. */
static Vertex* vertex_at(PolyList* list, uint32_t i) {

View File

@ -0,0 +1,247 @@
#pragma once
#include "tools/test.h"
#include "tools/gl_test.h"
#include <stdint.h>
#include <climits>
#include <vector>
#include <GL/gl.h>
#include <GL/glkos.h>
#include "GL/private.h"
#include "GL/state.h"
#include "containers/aligned_vector.h"
/* =========================================================================
* TexCoordFormatTests
*
* Coverage for the texture-coordinate attribute readers (GL/attributes.c).
* The supported glTexCoordPointer types each normalise differently:
* GL_FLOAT / GL_DOUBLE - passed through
* GL_UNSIGNED_BYTE - divided by 255
* GL_SHORT - divided by SHRT_MAX (32767)
* GL_INT - passed through (raw)
* Each is compared against an equivalent GL_FLOAT draw so the per-type scaling
* is pinned down. Immediate-mode glTexCoord2f and the disabled-array (zeroed)
* case are covered too.
* =========================================================================*/
class TexCoordFormatTests : public GLTestCase {
public:
void set_up() {
GLTestCase::set_up();
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
}
static void reset_list() {
aligned_vector_clear(&OP_LIST.vector);
_glGPUStateMarkDirty();
}
static std::vector<Vertex> captured() {
std::vector<Vertex> out;
uint32_t n = aligned_vector_size(&OP_LIST.vector);
for(uint32_t i = 0; i < n; ++i) {
Vertex* v = (Vertex*) aligned_vector_at(&OP_LIST.vector, i);
if(v->flags == GPU_CMD_VERTEX || v->flags == GPU_CMD_VERTEX_EOL) {
out.push_back(*v);
}
}
return out;
}
void assert_uvs_match(const std::vector<Vertex>& a, const std::vector<Vertex>& b) {
assert_equal(a.size(), b.size());
for(size_t i = 0; i < a.size(); ++i) {
assert_close(a[i].uv[0], b[i].uv[0], 0.0005f);
assert_close(a[i].uv[1], b[i].uv[1], 0.0005f);
}
}
/* A fixed triangle for the position data. */
static const GLfloat* positions() {
static const GLfloat verts[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f,
};
return verts;
}
std::vector<Vertex> draw_with_float_uv(const GLfloat* uv) {
reset_list();
glVertexPointer(3, GL_FLOAT, 0, positions());
glTexCoordPointer(2, GL_FLOAT, 0, uv);
glDrawArrays(GL_TRIANGLES, 0, 3);
return captured();
}
/* --------------------------------------------------------- float basic */
void test_float_uv_passthrough() {
GLfloat uv[] = { 0.0f, 0.0f, 1.0f, 0.0f, 0.5f, 1.0f };
std::vector<Vertex> v = draw_with_float_uv(uv);
assert_equal(v.size(), (size_t) 3);
assert_close(v[0].uv[0], 0.0f, 0.0005f);
assert_close(v[1].uv[0], 1.0f, 0.0005f);
assert_close(v[2].uv[1], 1.0f, 0.0005f);
}
/* --------------------------------------------------------- short / 32767 */
void test_short_uv_is_scaled_by_shrt_max() {
GLshort suv[] = {
0, 0,
SHRT_MAX, 0,
SHRT_MAX / 2, SHRT_MAX,
};
GLfloat fuv[] = {
0.0f, 0.0f,
(float) SHRT_MAX / SHRT_MAX, 0.0f,
(float)(SHRT_MAX / 2) / SHRT_MAX, (float) SHRT_MAX / SHRT_MAX,
};
std::vector<Vertex> fref = draw_with_float_uv(fuv);
reset_list();
glVertexPointer(3, GL_FLOAT, 0, positions());
glTexCoordPointer(2, GL_SHORT, 0, suv);
glDrawArrays(GL_TRIANGLES, 0, 3);
std::vector<Vertex> sres = captured();
assert_uvs_match(fref, sres);
}
/* --------------------------------------------------------- ubyte / 255 */
void test_ubyte_uv_is_scaled_by_255() {
GLubyte buv[] = { 0, 0, 255, 0, 128, 255 };
GLfloat fuv[] = {
0.0f, 0.0f,
255.0f/255.0f, 0.0f,
128.0f/255.0f, 255.0f/255.0f,
};
std::vector<Vertex> fref = draw_with_float_uv(fuv);
reset_list();
glVertexPointer(3, GL_FLOAT, 0, positions());
glTexCoordPointer(2, GL_UNSIGNED_BYTE, 0, buv);
glDrawArrays(GL_TRIANGLES, 0, 3);
std::vector<Vertex> bres = captured();
assert_uvs_match(fref, bres);
}
/* --------------------------------------------------------- int raw */
void test_int_uv_is_raw() {
GLint iuv[] = { 0, 0, 1, 0, 0, 1 };
GLfloat fuv[] = { 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f };
std::vector<Vertex> fref = draw_with_float_uv(fuv);
reset_list();
glVertexPointer(3, GL_FLOAT, 0, positions());
glTexCoordPointer(2, GL_INT, 0, iuv);
glDrawArrays(GL_TRIANGLES, 0, 3);
std::vector<Vertex> ires = captured();
assert_uvs_match(fref, ires);
}
/* --------------------------------------------------------- double */
void test_double_uv_passthrough() {
GLdouble duv[] = { 0.0, 0.0, 1.0, 0.0, 0.5, 1.0 };
GLfloat fuv[] = { 0.0f, 0.0f, 1.0f, 0.0f, 0.5f, 1.0f };
std::vector<Vertex> fref = draw_with_float_uv(fuv);
reset_list();
glVertexPointer(3, GL_FLOAT, 0, positions());
glTexCoordPointer(2, GL_DOUBLE, 0, duv);
glDrawArrays(GL_TRIANGLES, 0, 3);
std::vector<Vertex> dres = captured();
assert_uvs_match(fref, dres);
}
/* --------------------------------------------------------- stride */
void test_uv_stride_is_respected() {
GLfloat tight[] = { 0.0f, 0.0f, 1.0f, 0.0f, 0.5f, 1.0f };
GLfloat padded[] = {
0.0f, 0.0f, 77.0f,
1.0f, 0.0f, 77.0f,
0.5f, 1.0f, 77.0f,
};
std::vector<Vertex> rt = draw_with_float_uv(tight);
reset_list();
glVertexPointer(3, GL_FLOAT, 0, positions());
glTexCoordPointer(2, GL_FLOAT, 3 * sizeof(GLfloat), padded);
glDrawArrays(GL_TRIANGLES, 0, 3);
std::vector<Vertex> rp = captured();
assert_uvs_match(rt, rp);
}
/* --------------------------------------------------------- disabled */
/* With the texture-coordinate array disabled, every vertex takes the
* current texture coordinate (set via glTexCoord), matching fixed-function
* GL semantics. */
void test_disabled_coord_array_uses_current_texcoord() {
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
glTexCoord2f(0.25f, 0.75f);
reset_list();
glVertexPointer(3, GL_FLOAT, 0, positions());
glDrawArrays(GL_TRIANGLES, 0, 3);
std::vector<Vertex> v = captured();
assert_equal(v.size(), (size_t) 3);
for(size_t i = 0; i < v.size(); ++i) {
assert_close(v[i].uv[0], 0.25f, 0.0005f);
assert_close(v[i].uv[1], 0.75f, 0.0005f);
}
}
/* --------------------------------------------------------- immediate */
void test_immediate_texcoord2f_reaches_vertex() {
reset_list();
glBegin(GL_TRIANGLES);
glTexCoord2f(0.0f, 0.0f); glVertex3f(-0.5f, -0.5f, 0.0f);
glTexCoord2f(1.0f, 0.0f); glVertex3f( 0.5f, -0.5f, 0.0f);
glTexCoord2f(0.5f, 1.0f); glVertex3f( 0.0f, 0.5f, 0.0f);
glEnd();
std::vector<Vertex> v = captured();
assert_equal(v.size(), (size_t) 3);
assert_close(v[0].uv[0], 0.0f, 0.0005f);
assert_close(v[0].uv[1], 0.0f, 0.0005f);
assert_close(v[1].uv[0], 1.0f, 0.0005f);
assert_close(v[2].uv[0], 0.5f, 0.0005f);
assert_close(v[2].uv[1], 1.0f, 0.0005f);
}
/* Immediate texcoords must match the equivalent vertex-array draw. */
void test_immediate_texcoord_matches_array() {
GLfloat uv[] = { 0.0f, 0.0f, 1.0f, 0.0f, 0.5f, 1.0f };
std::vector<Vertex> arr = draw_with_float_uv(uv);
reset_list();
glBegin(GL_TRIANGLES);
glTexCoord2f(0.0f, 0.0f); glVertex3f(-0.5f, -0.5f, 0.0f);
glTexCoord2f(1.0f, 0.0f); glVertex3f( 0.5f, -0.5f, 0.0f);
glTexCoord2f(0.5f, 1.0f); glVertex3f( 0.0f, 0.5f, 0.0f);
glEnd();
std::vector<Vertex> imm = captured();
assert_uvs_match(arr, imm);
}
};

View File

@ -0,0 +1,321 @@
#pragma once
#include "tools/test.h"
#include "tools/gl_test.h"
#include <stdint.h>
#include <vector>
#include <GL/gl.h>
#include <GL/glext.h>
#include <GL/glkos.h>
#include "GL/private.h"
/* =========================================================================
* TextureFormatTests
*
* Regression coverage for texture *loading*: that glTexImage2D / glTexSubImage2D
* pick the right internal format and, crucially, that the host->PVR pixel
* conversion produces the exact expected bytes. These checks are byte-exact and
* fully deterministic, so they pin down the conversion routines (RGB565,
* ARGB4444, ARGB1555, RGBA8, RED/ALPHA/LUMINANCE sources, paletted) against
* accidental changes.
*
* Twiddling is left OFF for the byte-exact tests so the stored data is a plain
* row-major array of 16/32-bit texels we can index directly. Twiddled layouts
* are validated at the internal-format level only.
* =========================================================================*/
class TextureFormatTests : public GLTestCase {
public:
GLuint tex = 0;
void set_up() {
GLTestCase::set_up();
glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_2D, tex);
}
void tear_down() {
glDeleteTextures(1, &tex);
GLTestCase::tear_down();
}
/* The converted 16bpp texel array for the currently bound texture. */
static const uint16_t* data16() {
return (const uint16_t*) _glGetBoundTexture()->data;
}
static const uint8_t* data8() {
return (const uint8_t*) _glGetBoundTexture()->data;
}
static GLint internal_format() {
GLint f;
glGetIntegerv(GL_TEXTURE_INTERNAL_FORMAT_KOS, &f);
return f;
}
/* ------------------------------------------------------ Internal format */
void test_rgb_ubyte_selects_rgb565() {
std::vector<uint8_t> img(8 * 8 * 3, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 8, 8, 0, GL_RGB, GL_UNSIGNED_BYTE, img.data());
assert_equal(glGetError(), GL_NO_ERROR);
assert_equal(internal_format(), GL_RGB565_KOS);
}
void test_rgba_ubyte_selects_argb4444() {
std::vector<uint8_t> img(8 * 8 * 4, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, img.data());
assert_equal(glGetError(), GL_NO_ERROR);
assert_equal(internal_format(), GL_ARGB4444_KOS);
}
void test_twiddle_enabled_selects_twiddled_format() {
std::vector<uint8_t> img(8 * 8 * 3, 0);
glEnable(GL_TEXTURE_TWIDDLE_KOS);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 8, 8, 0, GL_RGB, GL_UNSIGNED_BYTE, img.data());
glDisable(GL_TEXTURE_TWIDDLE_KOS);
assert_equal(glGetError(), GL_NO_ERROR);
assert_equal(internal_format(), GL_RGB565_TWID_KOS);
}
/* ----------------------------------------------------- Dimensions/state */
void test_dimensions_are_stored() {
std::vector<uint8_t> img(16 * 32 * 3, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 16, 32, 0, GL_RGB, GL_UNSIGNED_BYTE, img.data());
assert_equal(glGetError(), GL_NO_ERROR);
TextureObject* t = _glGetBoundTexture();
assert_equal((int) t->width, 16);
assert_equal((int) t->height, 32);
}
/* ------------------------------------------------- Byte-exact: RGB565 */
void test_rgb888_to_rgb565_packing() {
/* Four distinct texels with known RGB values. */
uint8_t img[8 * 8 * 3] = {0};
uint8_t colors[4][3] = {
{255, 0, 0}, /* red */
{ 0, 255, 0}, /* green */
{ 0, 0, 255}, /* blue */
{130, 70, 30}, /* arbitrary */
};
for(int i = 0; i < 4; ++i) {
img[i * 3 + 0] = colors[i][0];
img[i * 3 + 1] = colors[i][1];
img[i * 3 + 2] = colors[i][2];
}
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 8, 8, 0, GL_RGB, GL_UNSIGNED_BYTE, img);
assert_equal(glGetError(), GL_NO_ERROR);
const uint16_t* d = data16();
for(int i = 0; i < 4; ++i) {
uint16_t r = (colors[i][0] >> 3) & 0x1f;
uint16_t g = (colors[i][1] >> 2) & 0x3f;
uint16_t b = (colors[i][2] >> 3) & 0x1f;
uint16_t expected = (r << 11) | (g << 5) | b;
assert_equal(d[i], expected);
}
}
void test_red_ubyte_to_rgb565_only_sets_red() {
uint8_t img[8 * 8] = {0};
img[0] = 0xFF; /* full red */
img[1] = 0x80;
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB565_KOS, 8, 8, 0, GL_RED, GL_UNSIGNED_BYTE, img);
assert_equal(glGetError(), GL_NO_ERROR);
const uint16_t* d = data16();
/* _r8_to_rgb565: (r & 0xF8) << 8 -> only top 5 red bits, green/blue zero */
assert_equal(d[0], (uint16_t)((0xFF & 0xF8) << 8));
assert_equal(d[1], (uint16_t)((0x80 & 0xF8) << 8));
/* green/blue bits must be clear */
assert_equal((int)(d[0] & 0x07FF), 0);
}
/* ------------------------------------------------- Byte-exact: ARGB4444 */
void test_rgba8888_to_argb4444_packing() {
uint8_t img[8 * 8 * 4] = {0};
uint8_t colors[4][4] = {
{0xFF, 0x00, 0x00, 0xFF}, /* opaque red */
{0x00, 0xFF, 0x00, 0x80}, /* half green */
{0x00, 0x00, 0xFF, 0x00}, /* transp blue */
{0x12, 0x34, 0x56, 0x78}, /* arbitrary */
};
for(int i = 0; i < 4; ++i)
for(int c = 0; c < 4; ++c)
img[i * 4 + c] = colors[i][c];
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, img);
assert_equal(glGetError(), GL_NO_ERROR);
const uint16_t* d = data16();
for(int i = 0; i < 4; ++i) {
uint8_t r = colors[i][0], g = colors[i][1], b = colors[i][2], a = colors[i][3];
uint16_t expected = ((a & 0xF0) << 8) | ((r & 0xF0) << 4) | (g & 0xF0) | ((b & 0xF0) >> 4);
assert_equal(d[i], expected);
}
}
void test_alpha_ubyte_to_argb4444_replicates_alpha() {
uint8_t img[8 * 8] = {0};
img[0] = 0xFF;
img[1] = 0x00;
img[2] = 0x5A;
glTexImage2D(GL_TEXTURE_2D, 0, GL_ARGB4444_KOS, 8, 8, 0, GL_ALPHA, GL_UNSIGNED_BYTE, img);
assert_equal(glGetError(), GL_NO_ERROR);
const uint16_t* d = data16();
/* _a8_to_argb4444 replicates the top nibble across all 4 channels. */
for(int i = 0; i < 3; ++i) {
uint8_t a4 = (img[i] >> 4) & 0xF;
uint16_t expected = (a4 << 12) | (a4 << 8) | (a4 << 4) | a4;
assert_equal(d[i], expected);
}
}
/* GL_RGBA8 is not a native PVR texture format in GLdc; it is downgraded to
* ARGB4444. Pin that behaviour so a change is noticed. */
void test_rgba8_is_downgraded_to_argb4444() {
std::vector<uint8_t> img(8 * 8 * 4, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, img.data());
assert_equal(glGetError(), GL_NO_ERROR);
assert_equal(internal_format(), GL_ARGB4444_KOS);
}
/* ------------------------------------------------- glTexSubImage2D */
void test_subimage_updates_only_target_region() {
/* The 2-wide RGB sub-image below is tightly packed (6 bytes/row), so it
* does not satisfy the default GL_UNPACK_ALIGNMENT of 4 (which would pad
* each row to 8 bytes). Tell GL the rows are tightly packed. */
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
/* Start with an all-black 16x16 RGB565 texture. */
std::vector<uint8_t> img(16 * 16 * 3, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 16, 16, 0, GL_RGB, GL_UNSIGNED_BYTE, img.data());
assert_equal(glGetError(), GL_NO_ERROR);
/* Replace a 2x2 block at (4,4) with red. */
uint8_t sub[2 * 2 * 3];
for(int i = 0; i < 4; ++i) { sub[i*3+0] = 255; sub[i*3+1] = 0; sub[i*3+2] = 0; }
glTexSubImage2D(GL_TEXTURE_2D, 0, 4, 4, 2, 2, GL_RGB, GL_UNSIGNED_BYTE, sub);
assert_equal(glGetError(), GL_NO_ERROR);
const uint16_t* d = data16();
uint16_t red565 = (uint16_t)((0xFF >> 3) << 11);
/* Updated texels are red, a neighbour outside the region is still black. */
assert_equal(d[4 * 16 + 4], red565);
assert_equal(d[4 * 16 + 5], red565);
assert_equal(d[5 * 16 + 4], red565);
assert_equal(d[5 * 16 + 5], red565);
assert_equal(d[4 * 16 + 6], (uint16_t) 0); /* just right of region */
assert_equal(d[0], (uint16_t) 0); /* corner untouched */
}
/* ------------------------------------------------- Paletted textures */
void test_color_index8_is_paletted() {
std::vector<uint8_t> img(8 * 8, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_COLOR_INDEX8_EXT, 8, 8, 0,
GL_COLOR_INDEX, GL_UNSIGNED_BYTE, img.data());
assert_equal(glGetError(), GL_NO_ERROR);
TextureObject* t = _glGetBoundTexture();
assert_true(t->isPaletted);
/* Paletted formats are always twiddled internally. */
assert_equal(t->internalFormat, GL_COLOR_INDEX8_TWID_KOS);
}
void test_color_table_then_indexed_texture_no_error() {
/* A 4 entry RGBA palette. */
uint8_t palette[4 * 4] = {
255, 0, 0, 255,
0, 255, 0, 255,
0, 0, 255, 255,
255, 255, 255, 255,
};
glColorTableEXT(GL_TEXTURE_2D, GL_RGBA8, 4, GL_RGBA, GL_UNSIGNED_BYTE, palette);
assert_equal(glGetError(), GL_NO_ERROR);
uint8_t indices[8 * 8];
for(int i = 0; i < 8 * 8; ++i) indices[i] = i & 3;
glTexImage2D(GL_TEXTURE_2D, 0, GL_COLOR_INDEX8_EXT, 8, 8, 0,
GL_COLOR_INDEX, GL_UNSIGNED_BYTE, indices);
assert_equal(glGetError(), GL_NO_ERROR);
TextureObject* t = _glGetBoundTexture();
assert_is_not_null(t->palette);
}
/* ------------------------------------------------- Error handling */
void test_non_power_of_two_width_raises_invalid_value() {
std::vector<uint8_t> img(24 * 8 * 3, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 24, 8, 0, GL_RGB, GL_UNSIGNED_BYTE, img.data());
assert_equal(glGetError(), GL_INVALID_VALUE);
}
void test_oversized_texture_raises_invalid_value() {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 2048, 8, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
assert_equal(glGetError(), GL_INVALID_VALUE);
}
void test_invalid_target_raises_invalid_enum() {
std::vector<uint8_t> img(8 * 8 * 3, 0);
/* Any target other than GL_TEXTURE_2D is unsupported. */
glTexImage2D((GLenum) 0x0DE0 /* GL_TEXTURE_1D */, 0, GL_RGB, 8, 8, 0,
GL_RGB, GL_UNSIGNED_BYTE, img.data());
assert_equal(glGetError(), GL_INVALID_ENUM);
}
/* ------------------------------------------------- Multiple textures */
void test_two_textures_keep_independent_data() {
GLuint a = 0, b = 0;
glGenTextures(1, &a);
glGenTextures(1, &b);
uint8_t reds[8 * 8 * 3];
uint8_t blues[8 * 8 * 3];
for(int i = 0; i < 8 * 8; ++i) {
reds[i*3+0] = 255; reds[i*3+1] = 0; reds[i*3+2] = 0;
blues[i*3+0] = 0; blues[i*3+1] = 0; blues[i*3+2] = 255;
}
glBindTexture(GL_TEXTURE_2D, a);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 8, 8, 0, GL_RGB, GL_UNSIGNED_BYTE, reds);
glBindTexture(GL_TEXTURE_2D, b);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 8, 8, 0, GL_RGB, GL_UNSIGNED_BYTE, blues);
assert_equal(glGetError(), GL_NO_ERROR);
uint16_t red565 = (uint16_t)((0xFF >> 3) << 11);
uint16_t blue565 = (uint16_t)((0xFF >> 3));
glBindTexture(GL_TEXTURE_2D, a);
assert_equal(data16()[0], red565);
glBindTexture(GL_TEXTURE_2D, b);
assert_equal(data16()[0], blue565);
glDeleteTextures(1, &a);
glDeleteTextures(1, &b);
}
/* Re-uploading a different size to the same texture must reallocate and
* store the new dimensions. */
void test_reupload_different_size_updates_dimensions() {
std::vector<uint8_t> img(8 * 8 * 3, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 8, 8, 0, GL_RGB, GL_UNSIGNED_BYTE, img.data());
std::vector<uint8_t> img2(32 * 32 * 3, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 32, 32, 0, GL_RGB, GL_UNSIGNED_BYTE, img2.data());
assert_equal(glGetError(), GL_NO_ERROR);
TextureObject* t = _glGetBoundTexture();
assert_equal((int) t->width, 32);
assert_equal((int) t->height, 32);
}
};

191
tests/test_vertex_colors.h Normal file
View File

@ -0,0 +1,191 @@
#pragma once
#include "tools/test.h"
#include "tools/gl_test.h"
#include <stdint.h>
#include <vector>
#include <GL/gl.h>
#include <GL/glkos.h>
#include "GL/private.h"
#include "GL/state.h"
#include "containers/aligned_vector.h"
/* =========================================================================
* VertexColorTests
*
* Byte vertex-colour coverage for glColorPointer, with particular attention to
* GL_BGRA as specified by OpenGL 1.4.
*
* Per the spec, a colour array may use size = GL_BGRA (only valid with
* GL_UNSIGNED_BYTE). The four bytes are then stored in memory in B, G, R, A
* order but are interpreted as the R, G, B, A colour components i.e. the
* red and blue channels are swapped relative to a normal GL_RGBA array. Alpha
* stays in the last byte.
*
* GLdc stores the resulting colour per vertex as normalised floats in ARGB
* slot order (A8IDX / R8IDX / G8IDX / B8IDX). These tests draw a triangle and
* read the submitted vertices back out of OP_LIST to verify the byte->float
* conversion and the BGRA channel ordering.
* =========================================================================*/
class VertexColorTests : public GLTestCase {
public:
static const GLfloat* positions() {
static const GLfloat verts[] = {
-1.0f, -1.0f, 0.5f,
1.0f, -1.0f, 0.5f,
0.0f, 1.0f, 0.5f,
};
return verts;
}
/* Submitted (non-header) vertices currently in OP_LIST. */
static std::vector<Vertex> captured() {
std::vector<Vertex> out;
uint32_t n = aligned_vector_size(&OP_LIST.vector);
for(uint32_t i = 0; i < n; ++i) {
Vertex* v = (Vertex*) aligned_vector_at(&OP_LIST.vector, i);
if(v->flags == GPU_CMD_VERTEX || v->flags == GPU_CMD_VERTEX_EOL) {
out.push_back(*v);
}
}
return out;
}
/* Draw a single triangle whose vertices use the supplied colour array. */
std::vector<Vertex> draw_with_colors(GLint size, GLenum type, const void* colors) {
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
glVertexPointer(3, GL_FLOAT, 0, positions());
glColorPointer(size, type, 0, colors);
glDrawArrays(GL_TRIANGLES, 0, 3);
return captured();
}
void assert_argb_close(const Vertex& v, float r, float g, float b, float a) {
assert_close(v.argb[R8IDX], r, 0.005f);
assert_close(v.argb[G8IDX], g, 0.005f);
assert_close(v.argb[B8IDX], b, 0.005f);
assert_close(v.argb[A8IDX], a, 0.005f);
}
/* --------------------------------------------------- byte RGBA values */
/* size=4 GL_UNSIGNED_BYTE: each byte is normalised by 1/255 and kept in
* RGBA order. Use non-trivial values so a wrong scale or order shows up. */
void test_byte_rgba4_normalizes_and_keeps_order() {
GLubyte colors[] = {
10, 20, 30, 40,
200, 150, 100, 50,
255, 128, 1, 254,
};
std::vector<Vertex> v = draw_with_colors(4, GL_UNSIGNED_BYTE, colors);
assert_equal(v.size(), (size_t) 3);
assert_argb_close(v[0], 10/255.0f, 20/255.0f, 30/255.0f, 40/255.0f);
assert_argb_close(v[1], 200/255.0f, 150/255.0f, 100/255.0f, 50/255.0f);
assert_argb_close(v[2], 255/255.0f, 128/255.0f, 1/255.0f, 254/255.0f);
}
/* size=3 GL_UNSIGNED_BYTE: RGB normalised, alpha forced to 1.0. */
void test_byte_rgb3_forces_alpha_one() {
GLubyte colors[] = {
10, 20, 30,
200, 150, 100,
0, 0, 0,
};
std::vector<Vertex> v = draw_with_colors(3, GL_UNSIGNED_BYTE, colors);
assert_equal(v.size(), (size_t) 3);
assert_argb_close(v[0], 10/255.0f, 20/255.0f, 30/255.0f, 1.0f);
assert_argb_close(v[1], 200/255.0f, 150/255.0f, 100/255.0f, 1.0f);
assert_argb_close(v[2], 0.0f, 0.0f, 0.0f, 1.0f);
}
/* --------------------------------------------------- GL_BGRA (GL 1.4) */
/* GL_BGRA: memory order is B, G, R, A but maps to R, G, B, A. With distinct
* channel values this proves the red/blue swap (and that alpha is the last
* byte). */
void test_bgra_byte_channel_order() {
/* memory: B=10, G=20, R=30, A=200 -> colour R=30, G=20, B=10, A=200 */
GLubyte colors[] = {
10, 20, 30, 200,
10, 20, 30, 200,
10, 20, 30, 200,
};
std::vector<Vertex> v = draw_with_colors(GL_BGRA, GL_UNSIGNED_BYTE, colors);
assert_equal(v.size(), (size_t) 3);
for(size_t i = 0; i < v.size(); ++i) {
assert_argb_close(v[i], 30/255.0f, 20/255.0f, 10/255.0f, 200/255.0f);
}
}
/* A GL_BGRA array laid out as {B,G,R,A} must produce exactly the same
* submitted colour as the equivalent GL_RGBA array {R,G,B,A}. */
void test_bgra_matches_equivalent_rgba() {
/* Logical colour: R=30, G=20, B=10, A=200 */
GLubyte rgba[] = {
30, 20, 10, 200,
30, 20, 10, 200,
30, 20, 10, 200,
};
GLubyte bgra[] = {
10, 20, 30, 200,
10, 20, 30, 200,
10, 20, 30, 200,
};
std::vector<Vertex> rgba_v = draw_with_colors(4, GL_UNSIGNED_BYTE, rgba);
/* Fresh list for the BGRA draw. */
aligned_vector_clear(&OP_LIST.vector);
_glGPUStateMarkDirty();
std::vector<Vertex> bgra_v = draw_with_colors(GL_BGRA, GL_UNSIGNED_BYTE, bgra);
assert_equal(rgba_v.size(), bgra_v.size());
for(size_t i = 0; i < rgba_v.size(); ++i) {
assert_close(rgba_v[i].argb[R8IDX], bgra_v[i].argb[R8IDX], 0.005f);
assert_close(rgba_v[i].argb[G8IDX], bgra_v[i].argb[G8IDX], 0.005f);
assert_close(rgba_v[i].argb[B8IDX], bgra_v[i].argb[B8IDX], 0.005f);
assert_close(rgba_v[i].argb[A8IDX], bgra_v[i].argb[A8IDX], 0.005f);
}
}
/* Distinct per-vertex GL_BGRA colours must each map correctly. */
void test_bgra_per_vertex_distinct() {
GLubyte colors[] = {
/* B G R A -> R / G / B / A */
0, 0, 255, 255, /* red, opaque */
0, 255, 0, 128, /* green, half a */
255, 0, 0, 64, /* blue, quarter */
};
std::vector<Vertex> v = draw_with_colors(GL_BGRA, GL_UNSIGNED_BYTE, colors);
assert_equal(v.size(), (size_t) 3);
assert_argb_close(v[0], 1.0f, 0.0f, 0.0f, 255/255.0f); /* red */
assert_argb_close(v[1], 0.0f, 1.0f, 0.0f, 128/255.0f); /* green */
assert_argb_close(v[2], 0.0f, 0.0f, 1.0f, 64/255.0f); /* blue */
}
/* Alpha for GL_BGRA comes from the last byte, independent of the RGB
* channels. */
void test_bgra_alpha_is_independent() {
GLubyte colors[] = {
255, 255, 255, 0, /* white, fully transparent */
255, 255, 255, 0,
255, 255, 255, 0,
};
std::vector<Vertex> v = draw_with_colors(GL_BGRA, GL_UNSIGNED_BYTE, colors);
assert_equal(v.size(), (size_t) 3);
for(size_t i = 0; i < v.size(); ++i) {
assert_argb_close(v[i], 1.0f, 1.0f, 1.0f, 0.0f);
}
}
/* GL_BGRA is accepted by glColorPointer without raising an error. */
void test_bgra_size_is_accepted() {
GLubyte colors[] = { 1, 2, 3, 4 };
glColorPointer(GL_BGRA, GL_UNSIGNED_BYTE, 0, colors);
assert_equal(glGetError(), GL_NO_ERROR);
assert_equal(ATTRIB_LIST.colour.size, (GLint) GL_BGRA);
}
};

313
tests/test_vertex_formats.h Normal file
View File

@ -0,0 +1,313 @@
#pragma once
#include "tools/test.h"
#include "tools/gl_test.h"
#include <stdint.h>
#include <vector>
#include <GL/gl.h>
#include <GL/glkos.h>
#include "GL/private.h"
#include "GL/state.h"
#include "containers/aligned_vector.h"
/* =========================================================================
* VertexFormatTests
*
* Coverage for the vertex-position attribute readers used by glDrawArrays /
* glDrawElements (GL/attributes.c). Each supported pointer type (float, short,
* int, byte, double) and size (2 / 3) is exercised, and the *submitted* vertex
* positions are compared against an equivalent GL_FLOAT draw.
*
* This pins down the per-type interpretation, e.g. that GL_SHORT/GL_INT are
* read as raw values while GL_UNSIGNED_BYTE is normalised by 1/255, so a
* regression in any reader is caught without depending on the exact viewport
* matrix maths.
* =========================================================================*/
class VertexFormatTests : public GLTestCase {
public:
void set_up() {
GLTestCase::set_up();
glEnableClientState(GL_VERTEX_ARRAY);
}
/* Empty OP_LIST and force a fresh poly header on the next draw. */
static void reset_list() {
aligned_vector_clear(&OP_LIST.vector);
_glGPUStateMarkDirty();
}
/* All vertex-flagged entries currently in OP_LIST (headers skipped). */
static std::vector<Vertex> captured() {
std::vector<Vertex> out;
uint32_t n = aligned_vector_size(&OP_LIST.vector);
for(uint32_t i = 0; i < n; ++i) {
Vertex* v = (Vertex*) aligned_vector_at(&OP_LIST.vector, i);
if(v->flags == GPU_CMD_VERTEX || v->flags == GPU_CMD_VERTEX_EOL) {
out.push_back(*v);
}
}
return out;
}
void assert_positions_match(const std::vector<Vertex>& a, const std::vector<Vertex>& b) {
assert_equal(a.size(), b.size());
for(size_t i = 0; i < a.size(); ++i) {
assert_close(a[i].xyz[0], b[i].xyz[0], 0.01f);
assert_close(a[i].xyz[1], b[i].xyz[1], 0.01f);
assert_close(a[i].xyz[2], b[i].xyz[2], 0.01f);
assert_close(a[i].w, b[i].w, 0.01f);
}
}
/* --------------------------------------------------------- float (ref) */
void test_float_size3_basic_submission() {
GLfloat verts[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f,
};
reset_list();
glVertexPointer(3, GL_FLOAT, 0, verts);
glDrawArrays(GL_TRIANGLES, 0, 3);
std::vector<Vertex> v = captured();
assert_equal(v.size(), (size_t) 3);
/* w must be 1 for an identity transform of a z=0 triangle. */
assert_close(v[0].w, 1.0f, 0.001f);
}
/* --------------------------------------------------------- short == float */
void test_short_matches_equivalent_float() {
GLshort sverts[] = { 2, 3, 5, 7, 11, 1 }; /* size 2 */
GLfloat fverts[] = { 2, 3, 5, 7, 11, 1 };
reset_list();
glVertexPointer(2, GL_FLOAT, 0, fverts);
glDrawArrays(GL_TRIANGLES, 0, 3);
std::vector<Vertex> fref = captured();
reset_list();
glVertexPointer(2, GL_SHORT, 0, sverts);
glDrawArrays(GL_TRIANGLES, 0, 3);
std::vector<Vertex> sres = captured();
assert_positions_match(fref, sres);
}
/* --------------------------------------------------------- int == float */
void test_int_matches_equivalent_float() {
GLint iverts[] = { 1, 4, 6, 2, 3, 9 };
GLfloat fverts[] = { 1, 4, 6, 2, 3, 9 };
reset_list();
glVertexPointer(2, GL_FLOAT, 0, fverts);
glDrawArrays(GL_TRIANGLES, 0, 3);
std::vector<Vertex> fref = captured();
reset_list();
glVertexPointer(2, GL_INT, 0, iverts);
glDrawArrays(GL_TRIANGLES, 0, 3);
std::vector<Vertex> ires = captured();
assert_positions_match(fref, ires);
}
/* --------------------------------------------------------- double == float */
void test_double_matches_equivalent_float() {
GLdouble dverts[] = { -0.5, -0.5, 0.5, -0.5, 0.0, 0.5 };
GLfloat fverts[] = { -0.5f, -0.5f, 0.5f, -0.5f, 0.0f, 0.5f };
reset_list();
glVertexPointer(2, GL_FLOAT, 0, fverts);
glDrawArrays(GL_TRIANGLES, 0, 3);
std::vector<Vertex> fref = captured();
reset_list();
glVertexPointer(2, GL_DOUBLE, 0, dverts);
glDrawArrays(GL_TRIANGLES, 0, 3);
std::vector<Vertex> dres = captured();
assert_positions_match(fref, dres);
}
/* GL_UNSIGNED_BYTE positions are normalised by 1/255. */
void test_ubyte_matches_normalized_float() {
GLubyte bverts[] = { 10, 20, 30, 40, 50, 60 };
GLfloat fverts[] = {
10.0f/255.0f, 20.0f/255.0f,
30.0f/255.0f, 40.0f/255.0f,
50.0f/255.0f, 60.0f/255.0f,
};
reset_list();
glVertexPointer(2, GL_FLOAT, 0, fverts);
glDrawArrays(GL_TRIANGLES, 0, 3);
std::vector<Vertex> fref = captured();
reset_list();
glVertexPointer(2, GL_UNSIGNED_BYTE, 0, bverts);
glDrawArrays(GL_TRIANGLES, 0, 3);
std::vector<Vertex> bres = captured();
assert_positions_match(fref, bres);
}
/* --------------------------------------------------------- size 2 vs 3 */
/* A size-2 pointer must produce z == 0 (and therefore match a size-3 draw
* whose z components are all zero). */
void test_size2_implies_zero_z() {
GLfloat v2[] = { -0.5f, -0.5f, 0.5f, -0.5f, 0.0f, 0.5f };
GLfloat v3[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f,
};
reset_list();
glVertexPointer(3, GL_FLOAT, 0, v3);
glDrawArrays(GL_TRIANGLES, 0, 3);
std::vector<Vertex> r3 = captured();
reset_list();
glVertexPointer(2, GL_FLOAT, 0, v2);
glDrawArrays(GL_TRIANGLES, 0, 3);
std::vector<Vertex> r2 = captured();
assert_positions_match(r3, r2);
}
/* --------------------------------------------------------- stride */
/* An interleaved array with padding between vertices must read the same
* positions as a tightly-packed one. */
void test_custom_stride_is_respected() {
GLfloat tight[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f,
};
/* Same positions, but each followed by 2 padding floats (stride 20). */
GLfloat padded[] = {
-0.5f, -0.5f, 0.0f, 99.0f, 99.0f,
0.5f, -0.5f, 0.0f, 99.0f, 99.0f,
0.0f, 0.5f, 0.0f, 99.0f, 99.0f,
};
reset_list();
glVertexPointer(3, GL_FLOAT, 0, tight);
glDrawArrays(GL_TRIANGLES, 0, 3);
std::vector<Vertex> rt = captured();
reset_list();
glVertexPointer(3, GL_FLOAT, 5 * sizeof(GLfloat), padded);
glDrawArrays(GL_TRIANGLES, 0, 3);
std::vector<Vertex> rp = captured();
assert_positions_match(rt, rp);
}
/* --------------------------------------------------------- glDrawElements */
void test_draw_elements_matches_draw_arrays() {
GLfloat verts[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f,
};
GLubyte indices[] = { 0, 1, 2 };
reset_list();
glVertexPointer(3, GL_FLOAT, 0, verts);
glDrawArrays(GL_TRIANGLES, 0, 3);
std::vector<Vertex> da = captured();
reset_list();
glVertexPointer(3, GL_FLOAT, 0, verts);
glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_BYTE, indices);
std::vector<Vertex> de = captured();
assert_positions_match(da, de);
}
/* glDrawElements honours the index order. */
void test_draw_elements_reorders_vertices() {
GLfloat verts[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f,
};
GLubyte forward[] = { 0, 1, 2 };
GLubyte reversed[] = { 2, 1, 0 };
reset_list();
glVertexPointer(3, GL_FLOAT, 0, verts);
glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_BYTE, forward);
std::vector<Vertex> f = captured();
reset_list();
glVertexPointer(3, GL_FLOAT, 0, verts);
glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_BYTE, reversed);
std::vector<Vertex> r = captured();
assert_equal(f.size(), (size_t) 3);
/* First of forward == last of reversed. */
assert_close(f[0].xyz[0], r[2].xyz[0], 0.01f);
assert_close(f[0].xyz[1], r[2].xyz[1], 0.01f);
assert_close(f[2].xyz[0], r[0].xyz[0], 0.01f);
}
/* --------------------------------------------------------- immediate mode */
/* glVertex2f(x,y) must equal glVertex3f(x,y,0). */
void test_immediate_vertex2f_equals_vertex3f_zero_z() {
reset_list();
glBegin(GL_TRIANGLES);
glVertex3f(-0.5f, -0.5f, 0.0f);
glVertex3f( 0.5f, -0.5f, 0.0f);
glVertex3f( 0.0f, 0.5f, 0.0f);
glEnd();
std::vector<Vertex> v3 = captured();
reset_list();
glBegin(GL_TRIANGLES);
glVertex2f(-0.5f, -0.5f);
glVertex2f( 0.5f, -0.5f);
glVertex2f( 0.0f, 0.5f);
glEnd();
std::vector<Vertex> v2 = captured();
assert_positions_match(v3, v2);
}
/* Immediate mode must produce the same submission as an equivalent
* vertex-array draw. */
void test_immediate_matches_vertex_array() {
GLfloat verts[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f,
};
reset_list();
glVertexPointer(3, GL_FLOAT, 0, verts);
glDrawArrays(GL_TRIANGLES, 0, 3);
std::vector<Vertex> arr = captured();
reset_list();
glBegin(GL_TRIANGLES);
glVertex3f(-0.5f, -0.5f, 0.0f);
glVertex3f( 0.5f, -0.5f, 0.0f);
glVertex3f( 0.0f, 0.5f, 0.0f);
glEnd();
std::vector<Vertex> imm = captured();
assert_positions_match(arr, imm);
}
};

110
tools/gl_test.h Normal file
View File

@ -0,0 +1,110 @@
/*
* Shared test fixture for GLdc tests that drive the GL API.
*
* On the Dreamcast the GPU/PVR (and the display) must be initialised exactly
* once: InitGPU() calls pvr_init() and ShutdownGPU() calls pvr_shutdown(), and
* tearing those down and back up between every test is both slow and fragile on
* real hardware / emulators. So this fixture:
*
* - initialises the GPU lazily on the first test and registers an atexit()
* handler to shut it down once, at program exit (window opens at start,
* closes at the end);
* - resets the mutable GL state to a known baseline before each test, without
* re-initialising the GPU.
*
* GL-driving test suites should derive from GLTestCase (instead of
* test::TestCase). If a suite needs extra per-test setup it should override
* set_up()/tear_down() and call GLTestCase::set_up()/tear_down() first.
*/
#pragma once
#include <cstdlib>
#include "tools/test.h"
#include <GL/gl.h>
#include <GL/glkos.h>
#include "GL/private.h"
#include "GL/state.h"
#include "containers/aligned_vector.h"
class GLTestCase : public test::TestCase {
public:
/* One-shot init flag (function-local static to keep this header-only). */
static bool& gpu_initialized() {
static bool v = false;
return v;
}
static void shutdown_gpu_at_exit() {
glKosShutdown();
}
static void ensure_gpu_initialized() {
if(gpu_initialized()) {
return;
}
GLdcConfig config;
glKosInitConfig(&config);
config.texture_twiddle = GL_FALSE;
glKosInitEx(&config);
gpu_initialized() = true;
/* Close the window / shut the PVR down once, when the process exits. */
atexit(GLTestCase::shutdown_gpu_at_exit);
}
/* Return all GL state to a predictable baseline. This deliberately avoids
* the allocating _glInit* helpers (immediate-mode buffer, texture/named
* arrays, framebuffers) so it is safe to call repeatedly. */
void reset_gl_state() {
aligned_vector_clear(&OP_LIST.vector);
aligned_vector_clear(&PT_LIST.vector);
aligned_vector_clear(&TR_LIST.vector);
glDisable(GL_TEXTURE_2D);
glDisable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
glDisable(GL_CULL_FACE);
glDisable(GL_LIGHTING);
glDisable(GL_FOG);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_TEXTURE_TWIDDLE_KOS);
glBlendFunc(GL_ONE, GL_ZERO);
glDepthFunc(GL_LESS);
glShadeModel(GL_SMOOTH);
/* Restore the spec default so a test that changes pixel-store state
* doesn't leak into the next (we no longer re-init per test). */
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
glBindTexture(GL_TEXTURE_2D, 0);
glMatrixMode(GL_PROJECTION); glLoadIdentity();
glMatrixMode(GL_MODELVIEW); glLoadIdentity();
glViewport(0, 0, 640, 480);
glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
glTexCoord2f(0.0f, 0.0f);
glNormal3f(0.0f, 0.0f, 1.0f);
ATTRIB_LIST.enabled = 0;
ATTRIB_LIST.dirty = ~0u;
_glGPUStateMarkDirty();
/* Drain any error raised while resetting. */
while(glGetError() != GL_NO_ERROR) {}
}
void set_up() {
ensure_gpu_initialized();
reset_gl_state();
}
void tear_down() {}
};

373
tools/golden.h Normal file
View File

@ -0,0 +1,373 @@
/*
* Golden-image test harness for GLdc.
*
* GLdc has a "software" backend that, on the desktop, normally renders the
* submitted Tile-Accelerator (TA) poly-lists through SDL's GPU renderer. That
* path needs a display and is not deterministic across machines/SDL versions,
* so it is unsuitable for committed "golden" reference images.
*
* Instead this harness contains a tiny, fully self-contained, deterministic CPU
* rasteriser that consumes exactly the same poly-lists the real backend does
* (OP_LIST / PT_LIST / TR_LIST, filled by glEnd / glDrawArrays / ...). It
* reproduces the backend's triangle-strip walking (see SceneListFinish in
* GL/platforms/software.c) and the perspective divide it performs, then fills
* triangles with Gouraud-interpolated vertex colour, optionally modulated by a
* decoded texture. The result is compared against a committed PPM reference.
*
* Because the rasteriser lives here (not in the library) it never changes when
* the library changes, so a diff in the rendered output reliably indicates a
* regression in GLdc's transform / colour / clipping / submission pipeline.
*
* Determinism notes:
* - Pure float maths, pixel-centre sampling, top-left-ish fill rule.
* - Painter's-order compositing (matching the software backend, which does
* no depth buffering): later triangles overwrite earlier ones for opaque
* lists; the transparent list alpha-blends.
* - Comparison is tolerant (per-channel + mismatch-fraction thresholds) so
* sub-LSB rounding differences between compilers do not cause flakiness,
* while real geometry/colour regressions are still caught.
*
* Usage from a test:
* golden::Image img(64, 64);
* img.clear(0, 0, 0);
* ... GL draw calls ...
* golden::rasterize_all_lists(img); // or rasterize_list(...)
* assert_true(golden::check(img, "name")); // compares tests/goldens/name.ppm
*
* Set the environment variable GLDC_UPDATE_GOLDENS=1 to (re)generate references.
* On a mismatch the actual and diff images are written next to the golden with
* .actual.ppm / .diff.ppm suffixes for inspection.
*/
#pragma once
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <string>
#include <vector>
#include "GL/private.h"
#include "GL/platform.h"
#include "containers/aligned_vector.h"
#ifndef GLDC_GOLDEN_DIR
/* Fallback; the real value is injected by CMake as a compile definition. */
#define GLDC_GOLDEN_DIR "goldens"
#endif
namespace golden {
/* ------------------------------------------------------------------ Image */
struct Image {
int w;
int h;
std::vector<uint8_t> rgb; /* w*h*3, row-major, top row first */
Image(int width, int height) : w(width), h(height), rgb(size_t(width) * height * 3, 0) {}
void clear(uint8_t r, uint8_t g, uint8_t b) {
for(size_t i = 0; i < rgb.size(); i += 3) {
rgb[i + 0] = r;
rgb[i + 1] = g;
rgb[i + 2] = b;
}
}
inline void put(int x, int y, uint8_t r, uint8_t g, uint8_t b) {
if(x < 0 || y < 0 || x >= w || y >= h) return;
size_t i = (size_t(y) * w + x) * 3;
rgb[i + 0] = r;
rgb[i + 1] = g;
rgb[i + 2] = b;
}
inline void get(int x, int y, uint8_t& r, uint8_t& g, uint8_t& b) const {
size_t i = (size_t(y) * w + x) * 3;
r = rgb[i + 0];
g = rgb[i + 1];
b = rgb[i + 2];
}
};
/* -------------------------------------------------------------------- PPM */
inline bool write_ppm(const std::string& path, const Image& img) {
FILE* f = fopen(path.c_str(), "wb");
if(!f) return false;
fprintf(f, "P6\n%d %d\n255\n", img.w, img.h);
fwrite(img.rgb.data(), 1, img.rgb.size(), f);
fclose(f);
return true;
}
inline bool read_ppm(const std::string& path, Image& out) {
FILE* f = fopen(path.c_str(), "rb");
if(!f) return false;
char magic[3] = {0};
int w = 0, h = 0, maxv = 0;
if(fscanf(f, "%2s", magic) != 1 || strcmp(magic, "P6") != 0) { fclose(f); return false; }
if(fscanf(f, "%d %d %d", &w, &h, &maxv) != 3) { fclose(f); return false; }
fgetc(f); /* single whitespace after the header */
out = Image(w, h);
size_t got = fread(out.rgb.data(), 1, out.rgb.size(), f);
fclose(f);
return got == out.rgb.size();
}
/* ------------------------------------------------------- Texture decoding */
/* Decodes one texel of a non-twiddled 16bpp DC texture to RGBA 0-255.
* Only the formats useful for golden tests are supported; everything else
* falls back to opaque white so the modulate path still produces vertex
* colour. Twiddled/compressed/paletted formats should be exercised by the
* byte-exact texture-loading unit tests instead. */
struct Texel { uint8_t r, g, b, a; };
inline Texel decode_texel(const TextureObject* tex, int x, int y) {
Texel out = {255, 255, 255, 255};
if(!tex || !tex->data) return out;
const int w = tex->width;
const uint16_t* px = (const uint16_t*) tex->data;
uint16_t v = px[size_t(y) * w + x];
switch(tex->internalFormat) {
case GL_RGB565_KOS: {
uint8_t r5 = (v >> 11) & 0x1f, g6 = (v >> 5) & 0x3f, b5 = v & 0x1f;
out.r = (r5 << 3) | (r5 >> 2);
out.g = (g6 << 2) | (g6 >> 4);
out.b = (b5 << 3) | (b5 >> 2);
out.a = 255;
} break;
case GL_ARGB4444_KOS: {
uint8_t a4 = (v >> 12) & 0xf, r4 = (v >> 8) & 0xf, g4 = (v >> 4) & 0xf, b4 = v & 0xf;
out.a = (a4 << 4) | a4;
out.r = (r4 << 4) | r4;
out.g = (g4 << 4) | g4;
out.b = (b4 << 4) | b4;
} break;
case GL_ARGB1555_KOS: {
uint8_t a1 = (v >> 15) & 0x1, r5 = (v >> 10) & 0x1f, g5 = (v >> 5) & 0x1f, b5 = v & 0x1f;
out.a = a1 ? 255 : 0;
out.r = (r5 << 3) | (r5 >> 2);
out.g = (g5 << 3) | (g5 >> 2);
out.b = (b5 << 3) | (b5 >> 2);
} break;
default:
break; /* unsupported -> white */
}
return out;
}
/* ------------------------------------------------------------- Rasteriser */
struct RVertex {
float x, y; /* screen space (post perspective divide) */
float a, r, g, b; /* 0-1 */
float u, v; /* texture coords */
};
inline float edge(const RVertex& a, const RVertex& b, float px, float py) {
return (px - a.x) * (b.y - a.y) - (py - a.y) * (b.x - a.x);
}
inline int wrap_coord(int c, int n) {
c %= n;
if(c < 0) c += n;
return c;
}
inline void fill_triangle(Image& img, const RVertex& v0, const RVertex& v1,
const RVertex& v2, const TextureObject* tex, bool blend) {
float area = edge(v0, v1, v2.x, v2.y);
if(fabsf(area) < 1e-6f) return; /* degenerate */
float inv_area = 1.0f / area;
int minx = (int) floorf(fminf(v0.x, fminf(v1.x, v2.x)));
int maxx = (int) ceilf (fmaxf(v0.x, fmaxf(v1.x, v2.x)));
int miny = (int) floorf(fminf(v0.y, fminf(v1.y, v2.y)));
int maxy = (int) ceilf (fmaxf(v0.y, fmaxf(v1.y, v2.y)));
if(minx < 0) minx = 0;
if(miny < 0) miny = 0;
if(maxx > img.w) maxx = img.w;
if(maxy > img.h) maxy = img.h;
for(int y = miny; y < maxy; ++y) {
float py = y + 0.5f;
for(int x = minx; x < maxx; ++x) {
float px = x + 0.5f;
float w0 = edge(v1, v2, px, py) * inv_area;
float w1 = edge(v2, v0, px, py) * inv_area;
float w2 = edge(v0, v1, px, py) * inv_area;
/* Inside test that accepts either winding (no culling, like the
* software backend). */
bool inside = (w0 >= 0 && w1 >= 0 && w2 >= 0) ||
(w0 <= 0 && w1 <= 0 && w2 <= 0);
if(!inside) continue;
float a = w0 * v0.a + w1 * v1.a + w2 * v2.a;
float r = w0 * v0.r + w1 * v1.r + w2 * v2.r;
float g = w0 * v0.g + w1 * v1.g + w2 * v2.g;
float b = w0 * v0.b + w1 * v1.b + w2 * v2.b;
if(tex) {
float u = w0 * v0.u + w1 * v1.u + w2 * v2.u;
float v = w0 * v0.v + w1 * v1.v + w2 * v2.v;
int tx = wrap_coord((int) floorf(u * tex->width), tex->width);
int ty = wrap_coord((int) floorf(v * tex->height), tex->height);
Texel t = decode_texel(tex, tx, ty);
r *= t.r / 255.0f;
g *= t.g / 255.0f;
b *= t.b / 255.0f;
a *= t.a / 255.0f;
}
a = a < 0 ? 0 : (a > 1 ? 1 : a);
r = r < 0 ? 0 : (r > 1 ? 1 : r);
g = g < 0 ? 0 : (g > 1 ? 1 : g);
b = b < 0 ? 0 : (b > 1 ? 1 : b);
uint8_t sr = (uint8_t) lrintf(r * 255.0f);
uint8_t sg = (uint8_t) lrintf(g * 255.0f);
uint8_t sb = (uint8_t) lrintf(b * 255.0f);
if(blend) {
uint8_t dr, dg, db;
img.get(x, y, dr, dg, db);
sr = (uint8_t) lrintf(sr * a + dr * (1.0f - a));
sg = (uint8_t) lrintf(sg * a + dg * (1.0f - a));
sb = (uint8_t) lrintf(sb * a + db * (1.0f - a));
}
img.put(x, y, sr, sg, sb);
}
}
}
inline RVertex to_screen(const Vertex* v) {
/* Reproduce the software backend perspective divide (see
* _glPerspectiveDivideVertex). Vertices in the lists are already viewport
* transformed, so dividing x/y by w gives window coordinates. */
float inv_w = (v->w != 0.0f) ? 1.0f / v->w : 1.0f;
RVertex out;
out.x = v->xyz[0] * inv_w;
out.y = v->xyz[1] * inv_w;
out.a = v->argb[0];
out.r = v->argb[1];
out.g = v->argb[2];
out.b = v->argb[3];
out.u = v->uv[0];
out.v = v->uv[1];
return out;
}
/* Rasterise a single poly list. Triangle-strip extraction mirrors
* SceneListFinish() exactly so the same triangles are produced. */
inline void rasterize_list(Image& img, PolyList* list, const TextureObject* tex, bool blend) {
uint32_t n = aligned_vector_size(&list->vector);
if(n < 4) return;
uint32_t vidx = 0;
for(uint32_t i = 0; i < n; ++i) {
Vertex* v = (Vertex*) aligned_vector_at(&list->vector, i);
if((v->flags & GPU_CMD_POLYHDR) == GPU_CMD_POLYHDR) {
vidx = 0;
continue;
}
if(v->flags == GPU_CMD_VERTEX || v->flags == GPU_CMD_VERTEX_EOL) {
++vidx;
}
if(vidx > 2) {
Vertex* a = (Vertex*) aligned_vector_at(&list->vector, i - 2);
Vertex* b = (Vertex*) aligned_vector_at(&list->vector, i - 1);
RVertex r0 = to_screen(a);
RVertex r1 = to_screen(b);
RVertex r2 = to_screen(v);
fill_triangle(img, r0, r1, r2, tex, blend);
}
if(v->flags == GPU_CMD_VERTEX_EOL) {
vidx = 0;
}
}
}
/* Convenience: rasterise the opaque, punch-through and transparent lists in
* the order the backend submits them. */
inline void rasterize_all_lists(Image& img, const TextureObject* tex = NULL) {
rasterize_list(img, &OP_LIST, tex, false);
rasterize_list(img, &PT_LIST, tex, false);
rasterize_list(img, &TR_LIST, tex, true);
}
/* ------------------------------------------------------------- Comparison */
inline std::string golden_path(const std::string& name, const char* suffix = "") {
return std::string(GLDC_GOLDEN_DIR) + "/" + name + suffix + ".ppm";
}
/* Compare img against the committed golden. Returns true on match (or when a
* golden was just (re)generated). max_channel_diff is the largest per-channel
* absolute difference tolerated per pixel; max_bad_fraction is the fraction of
* pixels allowed to exceed that. */
inline bool check(const Image& img, const std::string& name,
int max_channel_diff = 2, double max_bad_fraction = 0.005) {
std::string path = golden_path(name);
const char* update = getenv("GLDC_UPDATE_GOLDENS");
Image golden(0, 0);
bool have_golden = read_ppm(path, golden);
if((update && update[0] == '1') || !have_golden) {
if(!write_ppm(path, img)) {
fprintf(stderr, "golden: failed to write %s\n", path.c_str());
return false;
}
fprintf(stderr, "golden: generated reference %s\n", path.c_str());
return true;
}
if(golden.w != img.w || golden.h != img.h) {
fprintf(stderr, "golden: size mismatch for %s (%dx%d vs %dx%d)\n",
name.c_str(), golden.w, golden.h, img.w, img.h);
write_ppm(golden_path(name, ".actual"), img);
return false;
}
size_t bad = 0;
int worst = 0;
Image diff(img.w, img.h);
for(size_t i = 0; i < img.rgb.size(); i += 3) {
int dr = abs(int(img.rgb[i + 0]) - int(golden.rgb[i + 0]));
int dg = abs(int(img.rgb[i + 1]) - int(golden.rgb[i + 1]));
int db = abs(int(img.rgb[i + 2]) - int(golden.rgb[i + 2]));
int d = dr > dg ? (dr > db ? dr : db) : (dg > db ? dg : db);
if(d > worst) worst = d;
if(d > max_channel_diff) {
++bad;
diff.rgb[i + 0] = 255; /* highlight differing pixels in red */
}
}
size_t total = img.rgb.size() / 3;
double frac = double(bad) / double(total);
if(frac > max_bad_fraction) {
fprintf(stderr,
"golden: MISMATCH %s — %zu/%zu px differ (%.3f%%), worst channel diff %d\n",
name.c_str(), bad, total, frac * 100.0, worst);
write_ppm(golden_path(name, ".actual"), img);
write_ppm(golden_path(name, ".diff"), diff);
return false;
}
return true;
}
} // namespace golden

View File

@ -351,7 +351,10 @@ public:
output += " ";
}
std::cout << output;
/* Flush the name before running so that, if a test hard-crashes
* (e.g. on real Dreamcast hardware), the last line printed
* identifies the offending test. */
std::cout << output << std::flush;
test();
std::cout << "\033[32m" << " OK " << "\033[0m" << std::endl;
junit_lines.push_back(" </testcase>\n");

View File

@ -165,7 +165,7 @@ def find_tests(files):
# If this subclasses TestCase, or it subclasses any of the already found testcase subclasses
# then add it to the list
if "TestCase" in subclass_names or "SimulantTestCase" in subclass_names or any(x[1] in subclasses[i][2] for x in test_case_subclasses):
if "TestCase" in subclass_names or "SimulantTestCase" in subclass_names or "GLTestCase" in subclass_names or any(x[1] in subclasses[i][2] for x in test_case_subclasses):
if subclasses[i] not in test_case_subclasses:
test_case_subclasses.append(subclasses[i])