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/aligned_vector.h"
|
||||||
#include "../containers/named_array.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_4BPP_PALETTE_SLOTS 16
|
||||||
#define MAX_GLDC_PALETTE_SLOTS 4
|
#define MAX_GLDC_PALETTE_SLOTS 4
|
||||||
#define MAX_GLDC_SHARED_PALETTES (MAX_GLDC_PALETTE_SLOTS*MAX_GLDC_4BPP_PALETTE_SLOTS)
|
#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 MAX(a,b) (((a)>(b))?(a):(b))
|
||||||
#define CLAMP( X, _MIN, _MAX ) ( (X)<(_MIN) ? (_MIN) : ((X)>(_MAX) ? (_MAX) : (X)) )
|
#define CLAMP( X, _MIN, _MAX ) ( (X)<(_MIN) ? (_MIN) : ((X)>(_MAX) ? (_MAX) : (X)) )
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#endif // PRIVATE_H
|
#endif // PRIVATE_H
|
||||||
|
|||||||
@ -2,6 +2,15 @@
|
|||||||
|
|
||||||
FILE(GLOB GL_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/test_*.h)
|
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})
|
INCLUDE_DIRECTORIES(${CMAKE_SOURCE_DIR})
|
||||||
|
|
||||||
SET(TEST_GENERATOR_BIN ${CMAKE_SOURCE_DIR}/tools/test_generator.py)
|
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})
|
add_executable(gldc_tests ${TEST_FILES} ${TEST_SOURCES} ${TEST_MAIN_FILENAME})
|
||||||
target_link_libraries(gldc_tests GL)
|
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)
|
if(NOT PLATFORM_DREAMCAST)
|
||||||
set_target_properties(
|
set_target_properties(
|
||||||
gldc_tests
|
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
|
#pragma once
|
||||||
|
|
||||||
#include "tools/test.h"
|
#include "tools/test.h"
|
||||||
|
#include "tools/gl_test.h"
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <GL/gl.h>
|
#include <GL/gl.h>
|
||||||
@ -21,21 +22,11 @@
|
|||||||
* index 2 = green (G8IDX)
|
* index 2 = green (G8IDX)
|
||||||
* index 3 = blue (B8IDX)
|
* index 3 = blue (B8IDX)
|
||||||
* =========================================================================*/
|
* =========================================================================*/
|
||||||
class GlColorTests : public test::TestCase {
|
class GlColorTests : public GLTestCase {
|
||||||
public:
|
public:
|
||||||
void set_up() {
|
/* GLTestCase::set_up() initialises the GPU once and resets the current
|
||||||
GLdcConfig config;
|
* colour to white, which is the known-good starting state these tests
|
||||||
glKosInitConfig(&config);
|
* expect. */
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* After set_up the current colour must be white (1,1,1,1). */
|
/* After set_up the current colour must be white (1,1,1,1). */
|
||||||
void test_default_color_is_white() {
|
void test_default_color_is_white() {
|
||||||
@ -211,20 +202,9 @@ public:
|
|||||||
* pointer, that stride=0 triggers the automatic stride calculation, and
|
* pointer, that stride=0 triggers the automatic stride calculation, and
|
||||||
* that invalid size arguments raise GL_INVALID_VALUE.
|
* that invalid size arguments raise GL_INVALID_VALUE.
|
||||||
* =========================================================================*/
|
* =========================================================================*/
|
||||||
class GlColorPointerTests : public test::TestCase {
|
class GlColorPointerTests : public GLTestCase {
|
||||||
public:
|
public:
|
||||||
void set_up() {
|
/* GLTestCase::set_up() already clears ATTRIB_LIST.enabled. */
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* size=4, GL_FLOAT: stride auto-calculated to 4*4=16. */
|
/* size=4, GL_FLOAT: stride auto-calculated to 4*4=16. */
|
||||||
void test_colorpointer_size4_float_stores_metadata() {
|
void test_colorpointer_size4_float_stores_metadata() {
|
||||||
@ -352,19 +332,8 @@ public:
|
|||||||
* Mirrors GlColorPointerTests for the secondary (offset) colour pointer
|
* Mirrors GlColorPointerTests for the secondary (offset) colour pointer
|
||||||
* stored in ATTRIB_LIST.s_color.
|
* stored in ATTRIB_LIST.s_color.
|
||||||
* =========================================================================*/
|
* =========================================================================*/
|
||||||
class GlSecondaryColorPointerTests : public test::TestCase {
|
class GlSecondaryColorPointerTests : public GLTestCase {
|
||||||
public:
|
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. */
|
/* size=4, GL_FLOAT: basic happy path. */
|
||||||
void test_secondary_colorpointer_size4_float_stores_metadata() {
|
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
|
* 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.
|
* dirty flag is updated, and that invalid enumerants raise GL_INVALID_ENUM.
|
||||||
* =========================================================================*/
|
* =========================================================================*/
|
||||||
class GlClientStateTests : public test::TestCase {
|
class GlClientStateTests : public GLTestCase {
|
||||||
public:
|
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. */
|
/* Enabling GL_COLOR_ARRAY must set COLOR_ENABLED_FLAG. */
|
||||||
void test_enable_color_array_sets_flag() {
|
void test_enable_color_array_sets_flag() {
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
#include "tools/test.h"
|
#include "tools/test.h"
|
||||||
|
#include "tools/gl_test.h"
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
@ -7,17 +10,14 @@
|
|||||||
#include <GL/glkos.h>
|
#include <GL/glkos.h>
|
||||||
|
|
||||||
|
|
||||||
class TexImage2DTests : public test::TestCase {
|
class TexImage2DTests : public GLTestCase {
|
||||||
public:
|
public:
|
||||||
uint8_t image_data[8 * 8 * 4] = {0};
|
uint8_t image_data[8 * 8 * 4] = {0};
|
||||||
uint8_t stride_image_data[96 * 48 * 4] = {0};
|
uint8_t stride_image_data[96 * 48 * 4] = {0};
|
||||||
uint8_t unpack_row_image_data[128 * 48 * 4] = {0};
|
uint8_t unpack_row_image_data[128 * 48 * 4] = {0};
|
||||||
|
|
||||||
void set_up() {
|
void set_up() {
|
||||||
GLdcConfig config;
|
GLTestCase::set_up();
|
||||||
glKosInitConfig(&config);
|
|
||||||
config.texture_twiddle = false;
|
|
||||||
glKosInitEx(&config);
|
|
||||||
|
|
||||||
/* Init image data so each texel RGBA value matches the
|
/* Init image data so each texel RGBA value matches the
|
||||||
* position in the array */
|
* 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() {
|
void test_rgb_to_rgb565() {
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 8, 8, 0, GL_RGB, GL_UNSIGNED_BYTE, image_data);
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 8, 8, 0, GL_RGB, GL_UNSIGNED_BYTE, image_data);
|
||||||
assert_equal(glGetError(), GL_NO_ERROR);
|
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
|
#pragma once
|
||||||
|
|
||||||
#include "tools/test.h"
|
#include "tools/test.h"
|
||||||
|
#include "tools/gl_test.h"
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <GL/gl.h>
|
#include <GL/gl.h>
|
||||||
@ -32,22 +33,10 @@
|
|||||||
/* =========================================================================
|
/* =========================================================================
|
||||||
* PVRVertexSubmissionTests
|
* PVRVertexSubmissionTests
|
||||||
* =========================================================================*/
|
* =========================================================================*/
|
||||||
class PVRVertexSubmissionTests : public test::TestCase {
|
class PVRVertexSubmissionTests : public GLTestCase {
|
||||||
public:
|
public:
|
||||||
void set_up() {
|
/* GLTestCase::set_up() gives a clean slate: GPU initialised once, colour
|
||||||
GLdcConfig config;
|
* reset to white and all client-state arrays disabled. */
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Convenience: cast element i of a list to Vertex*. */
|
/* Convenience: cast element i of a list to Vertex*. */
|
||||||
static Vertex* vertex_at(PolyList* list, uint32_t i) {
|
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 += " ";
|
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();
|
test();
|
||||||
std::cout << "\033[32m" << " OK " << "\033[0m" << std::endl;
|
std::cout << "\033[32m" << " OK " << "\033[0m" << std::endl;
|
||||||
junit_lines.push_back(" </testcase>\n");
|
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
|
# If this subclasses TestCase, or it subclasses any of the already found testcase subclasses
|
||||||
# then add it to the list
|
# 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:
|
if subclasses[i] not in test_case_subclasses:
|
||||||
test_case_subclasses.append(subclasses[i])
|
test_case_subclasses.append(subclasses[i])
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user