Merge branch 'master' into 'feature/kos-texture-stride'
# Conflicts: # tests/test_glteximage2d.h
This commit is contained in:
commit
cbc83deee0
10
GL/private.h
10
GL/private.h
@ -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
|
||||
|
||||
@ -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
127
tests/README.md
Normal 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.
|
||||
35
tests/goldens/bgra_vertex_colors_alpha.ppm
Normal file
35
tests/goldens/bgra_vertex_colors_alpha.ppm
Normal file
File diff suppressed because one or more lines are too long
BIN
tests/goldens/colored_quad_strip.ppm
Normal file
BIN
tests/goldens/colored_quad_strip.ppm
Normal file
Binary file not shown.
BIN
tests/goldens/gouraud_triangle.ppm
Normal file
BIN
tests/goldens/gouraud_triangle.ppm
Normal file
Binary file not shown.
BIN
tests/goldens/overlapping_opaque.ppm
Normal file
BIN
tests/goldens/overlapping_opaque.ppm
Normal file
Binary file not shown.
BIN
tests/goldens/solid_white_triangle.ppm
Normal file
BIN
tests/goldens/solid_white_triangle.ppm
Normal file
Binary file not shown.
BIN
tests/goldens/textured_quad.ppm
Normal file
BIN
tests/goldens/textured_quad.ppm
Normal file
Binary file not shown.
BIN
tests/goldens/transparent_over_opaque.ppm
Normal file
BIN
tests/goldens/transparent_over_opaque.ppm
Normal file
Binary file not shown.
BIN
tests/goldens/triangle_on_background.ppm
Normal file
BIN
tests/goldens/triangle_on_background.ppm
Normal file
Binary file not shown.
@ -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() {
|
||||
|
||||
@ -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);
|
||||
|
||||
243
tests/test_golden_rendering.h
Normal file
243
tests/test_golden_rendering.h
Normal 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"));
|
||||
}
|
||||
};
|
||||
@ -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) {
|
||||
|
||||
247
tests/test_texcoord_formats.h
Normal file
247
tests/test_texcoord_formats.h
Normal 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);
|
||||
}
|
||||
};
|
||||
321
tests/test_texture_formats.h
Normal file
321
tests/test_texture_formats.h
Normal 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
191
tests/test_vertex_colors.h
Normal 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
313
tests/test_vertex_formats.h
Normal 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
110
tools/gl_test.h
Normal 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
373
tools/golden.h
Normal 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
|
||||
@ -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");
|
||||
|
||||
@ -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])
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user