Skip to content

Commit 8c83c6c

Browse files
committed
Add a Color Format core option: 24-bit and 30-bit truecolor rendering
The software renderer wrote RGB565 unconditionally. That is fine for the palette's own colours but it quantises everything derived from them: the light ramp in V_Palette16 carries 64 weights per colour and the Smooth shading ramp 256, yet a 5-bit channel can only express about 17 distinct levels of either, so distance shading and smooth gradients band. Native 24/32-bit source art (PNG title cards, the help screen, the intermission backdrop) was narrowed on the way to the surface for the same reason. Add a "Color Format" core option selecting the output pixel format: 16bits RGB565 (default, the historical renderer) 24bits (truecolor) XRGB8888 30bits (HDR) XRGB2101010 Both truecolor formats are native pipelines rather than a conversion stage. The palette (V_PaletteTC), the composed colour tables and the Smooth ramp are all built at the output's channel width from the same 8-bit gamma-corrected palette source, so no value is ever produced in 565 and re-expanded, and no value passes through 8-bit on its way to 10-bit. Measured on the shading ramp, a 256-step gradient resolves to roughly 17 levels per channel at 16 bits, about 135 at 24 and close to the full 256 at 30 -- the gap widening in dark colours, which is where the banding is most visible. The renderer twin lives in r_drawtc.c. The column/span/wall-run drawers, composed-LUT machinery and dispatch tables mirror r_draw.c exactly with a 16->32-bit surface retype; the two 32-bit formats share that single instantiation, because an opaque drawer writes lut[texel] and is correct for any format given a format-correct table. Only the read-modify-write blend kernels carry per-format channel constants, so only those are compiled twice, from r_drawtcfmt.inl (SSE2 and NEON, with the scalar forms as the bit-exact references, matching the existing 565 kernels). Everything that wrote the surface directly is routed the same way: the direct sprite column, the voxel rasteriser, the skybox scratch, the plane and wall dynamic-light tints, the underwater tint, the wipe melt and the ACS overlay compositors. Several of those become simpler rather than more complex -- the ACS alpha compositors previously expanded the 565 destination to 8 bits, blended, and re-narrowed on store; in truecolor the blend just runs at the destination's own width. SURFACE_PIXEL_DEPTH becomes a runtime value (vid_pixelbytes). It was never used in preprocessor arithmetic, so this is a straight substitution; the cache and scratch buffers that size themselves from it needed no other change. The skybox scratch moves from a fixed 8 MB 16-bit static to a lazily sized allocation, which is smaller than before in the 16-bit build. Format selection happens once in retro_load_game, before any surface, palette or table is built. 30-bit is gated on the new RETRO_ENVIRONMENT_GET_SCREEN_10BPC_CAPABLE query rather than on SET_PIXEL_FORMAT alone: SET_PIXEL_FORMAT accepts XRGB2101010 unconditionally and the frontend silently narrows the frame when the driver cannot present 10 bits, and that narrowing truncates where our own XRGB8888 path rounds. So on a non-capable driver, emitting 10-bit is not merely wasted work but measurably worse output than rendering 24-bit; the core falls back to 24-bit and logs why. An older frontend that does not recognise the call returns false, which the contract defines as "no guarantee of native 10-bit" -- the same branch. A SET_PIXEL_FORMAT rejection still degrades 30 -> 24 -> 16 underneath. Note that XRGB2101010 here is deeper-precision SDR; the core emits no HDR transfer function or metadata. Also sync libretro-common with RetroArch f3fea37, which is where that environment call comes from. Only include/libretro.h differs from the vendored copy; the other commits in that range touch image/mp4/webm sources this core does not vendor. The 16-bit path is untouched and stays the reference: frame-hash output over Doom, Doom II, Heretic and zdcmp2.pk3 is byte-identical to the parent commit. 24-bit and 30-bit were checked over the same content through the title screen, the 3D scene, sprites, the status bar, the intermission and the wipes.
1 parent 7348269 commit 8c83c6c

20 files changed

Lines changed: 8497 additions & 128 deletions

Makefile.common

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ SOURCES_C += $(CORE_DIR)/am_map.c \
114114
$(CORE_DIR)/r_bsp.c \
115115
$(CORE_DIR)/r_data.c \
116116
$(CORE_DIR)/r_draw.c \
117+
$(CORE_DIR)/r_drawtc.c \
118+
$(CORE_DIR)/vid_mode.c \
117119
$(CORE_DIR)/r_main.c \
118120
$(CORE_DIR)/r_plane.c \
119121
$(CORE_DIR)/r_drawcmd.c \

README.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,12 +186,34 @@ the wad.
186186
them, with colours taken from the definition or derived from the texture
187187
itself.
188188

189+
### Colour depth
190+
191+
The **Color Format** core option selects the output pixel format: `16bits`
192+
(RGB565, the default and the historical renderer), `24bits (truecolor)`
193+
(XRGB8888) or `30bits (HDR)` (XRGB2101010). The truecolor formats are native
194+
pipelines, not a conversion stage — the palette, light ramps and composed
195+
colour tables are built at the output's channel width, so nothing is
196+
quantised to 565 on the way. This mainly removes the banding the 16-bit
197+
light ramp introduces in distance shading and in smooth gradients: a
198+
256-step shading ramp resolves to roughly 17 levels per channel at 16 bits,
199+
about 135 at 24, and near the full 256 at 30 — the gap being widest in dark
200+
colours, where the banding is most visible. Full-colour art (PNG title
201+
cards, the help screen, the intermission backdrop) is also blitted without
202+
losing precision.
203+
204+
`30bits` is used only when the frontend reports that it can present a
205+
10-bit surface natively; otherwise the core falls back to `24bits`, which
206+
rounds rather than truncates and so looks better than a narrowed 10-bit
207+
frame. Note that XRGB2101010 is deeper-precision SDR — the core emits no
208+
HDR transfer function or metadata.
209+
210+
Changing the option requires a restart.
211+
189212
### Not supported
190213

191214
- **ZScript** — no support. Mods whose gameplay lives in ZScript won't run it.
192215
- **Line / sector portals** — inert.
193216
- **3D models (MODELDEF)** — out of scope for the 8-bit software renderer.
194-
- **Truecolor rendering.**
195217

196218
The practical result: map-and-resource-driven ZDoom wads — new levels, sprite
197219
and texture replacements, ACS-scripted set pieces, reskinned monsters, Hexen-

libretro/libretro-common/include/libretro.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2715,6 +2715,34 @@ enum retro_mod
27152715
*/
27162716
#define RETRO_ENVIRONMENT_SET_SERIALIZATION_QUIRKS 87
27172717

2718+
/**
2719+
* Queries whether the active video driver can present a 10-bit-per-channel
2720+
* (30-bit) source surface end to end, i.e. whether a frame submitted as
2721+
* #RETRO_PIXEL_FORMAT_XRGB2101010 reaches the display without the frontend
2722+
* narrowing it to 8 bits per channel.
2723+
*
2724+
* Unlike SET_PIXEL_FORMAT, which accepts XRGB2101010 unconditionally and
2725+
* transparently down-converts when the driver cannot present 10-bit, this
2726+
* lets a core discover the real capability so it can avoid pointless work:
2727+
* a core that has both a 10-bit and an 8-bit output path should prefer the
2728+
* 8-bit path when this returns \c false, since emitting 10-bit only to have
2729+
* the frontend narrow it wastes effort and, for content that starts at 8
2730+
* bits, is a no-op round trip; going straight to 8 bits also rounds rather
2731+
* than truncates.
2732+
*
2733+
* The result may change across a driver reinit (e.g. the user switches
2734+
* video driver or toggles HDR), so a core that cares should query it when
2735+
* (re)choosing its pixel format rather than caching it indefinitely.
2736+
*
2737+
* @param[out] data <tt>bool *</tt>.
2738+
* Set to \c true if a 10-bit source surface is presented natively, \c false
2739+
* if XRGB2101010 frames are down-converted to 8-bit.
2740+
* @return \c true if the environment call is recognised (the value at
2741+
* \c data is then valid), \c false if it is unsupported (an older frontend);
2742+
* a core must treat "unsupported" as "no guarantee of native 10-bit".
2743+
*/
2744+
#define RETRO_ENVIRONMENT_GET_SCREEN_10BPC_CAPABLE (88 | RETRO_ENVIRONMENT_EXPERIMENTAL)
2745+
27182746
/**
27192747
* Result of \c RETRO_ENVIRONMENT_GET_MEMORY_STATUS.
27202748
*

libretro/libretro.c

Lines changed: 130 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#endif
3232

3333
#include "libretro_core_options.h"
34+
#include "../src/vid_mode.h"
3435

3536
/* prboom includes */
3637

@@ -936,32 +937,10 @@ void retro_init(void)
936937
midi_iface_valid = false;
937938
}
938939

939-
/* Negotiate pixel format with the frontend. The renderer is
940-
* hardcoded to write 16-bit pixels via VID_PAL16 -- the
941-
* surface depth is fixed at SURFACE_PIXEL_DEPTH=2 and every
942-
* draw column / span helper writes uint16_t directly. We
943-
* cannot fall through to XRGB1555 silently if RGB565 is
944-
* rejected; the output would be unspecified-format garbage
945-
* (with the wrong red/blue channel widths and an alpha bit
946-
* smeared across the green LSB). Log a hard error so the
947-
* user sees a visible diagnostic instead of a corrupted
948-
* screen. */
949-
{
950-
enum retro_pixel_format rgb565 = RETRO_PIXEL_FORMAT_RGB565;
951-
if (!environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &rgb565))
952-
{
953-
if (log_cb)
954-
log_cb(RETRO_LOG_ERROR,
955-
"Frontend rejected RGB565 pixel format -- this "
956-
"core requires it. Update RetroArch or your "
957-
"frontend to a build that supports RGB565.\n");
958-
}
959-
else if (log_cb)
960-
{
961-
log_cb(RETRO_LOG_DEBUG,
962-
"Frontend accepted RGB565 pixel format.\n");
963-
}
964-
}
940+
/* Pixel format is negotiated in retro_load_game, once the "Color
941+
* Format" core option has been read -- it must be settled before any
942+
* surface, palette or lookup table is built, and retro_init runs before
943+
* the frontend can answer GET_VARIABLE. */
965944

966945
if (environ_cb(RETRO_ENVIRONMENT_GET_INPUT_BITMASKS, NULL))
967946
libretro_supports_bitmasks = true;
@@ -1314,6 +1293,112 @@ static void update_audio_samplerate(void)
13141293
I_SetSoundRate(chosen);
13151294
}
13161295

1296+
/*
1297+
* I_NegotiatePixelFormat
1298+
*
1299+
* Settle the output pixel format for the whole session, from the "Color
1300+
* Format" core option. Must run before update_variables() computes
1301+
* SCREENPITCH and before any surface, palette or lookup table is built --
1302+
* SURFACE_PIXEL_DEPTH is derived from the result.
1303+
*
1304+
* The 30-bit path is gated on RETRO_ENVIRONMENT_GET_SCREEN_10BPC_CAPABLE
1305+
* rather than on SET_PIXEL_FORMAT alone. SET_PIXEL_FORMAT accepts
1306+
* XRGB2101010 unconditionally and the frontend silently narrows the frame
1307+
* to 8 bits when the active video driver cannot present a 10-bit surface,
1308+
* so acceptance says nothing about what reaches the display. Worse, that
1309+
* narrowing truncates, while our own XRGB8888 path rounds -- so on a
1310+
* non-capable driver the 10-bit renderer is not merely wasted work, it is
1311+
* measurably worse output than just rendering 24-bit. Query the real
1312+
* capability and drop to 24-bit when it is not there.
1313+
*
1314+
* An older frontend that does not recognise the call returns false, which
1315+
* the libretro contract defines as "no guarantee of native 10-bit" -- the
1316+
* safe default, and the same branch we take for a non-capable driver.
1317+
*/
1318+
static void I_NegotiatePixelFormat(void)
1319+
{
1320+
struct retro_variable var;
1321+
enum retro_pixel_format fmt;
1322+
int want = VID_MODE565;
1323+
1324+
var.key = "prboom-color_format";
1325+
var.value = NULL;
1326+
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
1327+
{
1328+
if (!strcmp(var.value, "24bits (truecolor)"))
1329+
want = VID_MODE8888;
1330+
else if (!strcmp(var.value, "30bits (HDR)"))
1331+
want = VID_MODE2101010;
1332+
}
1333+
1334+
if (want == VID_MODE2101010)
1335+
{
1336+
bool tenbit = false;
1337+
if (!environ_cb(RETRO_ENVIRONMENT_GET_SCREEN_10BPC_CAPABLE, &tenbit)
1338+
|| !tenbit)
1339+
{
1340+
want = VID_MODE8888;
1341+
if (log_cb)
1342+
log_cb(RETRO_LOG_INFO,
1343+
"Color Format: 30-bit requested, but the frontend does "
1344+
"not present a 10-bit source natively -- using 24-bit "
1345+
"truecolor instead (rounds rather than truncates).\n");
1346+
}
1347+
}
1348+
1349+
/* Ask for the chosen format, degrading if the frontend refuses it. */
1350+
for (;;)
1351+
{
1352+
if (want == VID_MODE2101010)
1353+
fmt = RETRO_PIXEL_FORMAT_XRGB2101010;
1354+
else if (want == VID_MODE8888)
1355+
fmt = RETRO_PIXEL_FORMAT_XRGB8888;
1356+
else
1357+
fmt = RETRO_PIXEL_FORMAT_RGB565;
1358+
1359+
if (environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &fmt))
1360+
break;
1361+
1362+
if (want == VID_MODE2101010)
1363+
{
1364+
want = VID_MODE8888;
1365+
if (log_cb)
1366+
log_cb(RETRO_LOG_WARN,
1367+
"Frontend rejected XRGB2101010; falling back to "
1368+
"XRGB8888.\n");
1369+
}
1370+
else if (want == VID_MODE8888)
1371+
{
1372+
want = VID_MODE565;
1373+
if (log_cb)
1374+
log_cb(RETRO_LOG_WARN,
1375+
"Frontend rejected XRGB8888; falling back to RGB565.\n");
1376+
}
1377+
else
1378+
{
1379+
/* RGB565 refused: the renderer cannot produce anything else that
1380+
* is guaranteed, so log loudly rather than emitting pixels in an
1381+
* unspecified format (wrong channel widths, smeared alpha). */
1382+
if (log_cb)
1383+
log_cb(RETRO_LOG_ERROR,
1384+
"Frontend rejected RGB565 pixel format -- this core "
1385+
"requires it. Update RetroArch or your frontend to a "
1386+
"build that supports RGB565.\n");
1387+
break;
1388+
}
1389+
}
1390+
1391+
vid_mode = want;
1392+
vid_pixelbytes = (want == VID_MODE565) ? 2 : 4;
1393+
1394+
if (log_cb)
1395+
log_cb(RETRO_LOG_INFO, "Color Format: %s (%d bytes/pixel).\n",
1396+
(want == VID_MODE2101010) ? "30-bit XRGB2101010"
1397+
: (want == VID_MODE8888) ? "24-bit XRGB8888"
1398+
: "16-bit RGB565",
1399+
vid_pixelbytes);
1400+
}
1401+
13171402
static void update_variables(bool startup)
13181403
{
13191404
struct retro_variable var;
@@ -2011,6 +2096,11 @@ bool retro_load_game(const struct retro_game_info *info)
20112096
else if (log_cb)
20122097
log_cb(RETRO_LOG_INFO, "Rumble environment not supported.\n");
20132098

2099+
/* Settle the pixel format first: update_variables() derives SCREENPITCH
2100+
* from SURFACE_PIXEL_DEPTH, and everything allocated below is sized by
2101+
* it. */
2102+
I_NegotiatePixelFormat();
2103+
20142104
update_variables(true);
20152105

20162106
argv[argc++] = strdup("prboom");
@@ -3587,14 +3677,20 @@ dbool I_StartDisplay(void)
35873677
fb.width = SCREENWIDTH;
35883678
fb.height = SCREENHEIGHT;
35893679
fb.pitch = 0;
3590-
fb.format = RETRO_PIXEL_FORMAT_RGB565;
3680+
fb.format = (vid_mode == VID_MODE2101010)
3681+
? RETRO_PIXEL_FORMAT_XRGB2101010
3682+
: (vid_mode == VID_MODE8888)
3683+
? RETRO_PIXEL_FORMAT_XRGB8888
3684+
: RETRO_PIXEL_FORMAT_RGB565;
35913685
fb.access_flags = RETRO_MEMORY_ACCESS_WRITE;
35923686
fb.memory_flags = 0;
35933687

3688+
{
3689+
const enum retro_pixel_format want_fmt = fb.format;
35943690
if (environ_cb(RETRO_ENVIRONMENT_GET_CURRENT_SOFTWARE_FRAMEBUFFER,
35953691
&fb)
35963692
&& fb.data
3597-
&& fb.format == RETRO_PIXEL_FORMAT_RGB565
3693+
&& fb.format == want_fmt
35983694
&& fb.pitch == (size_t)SCREENPITCH)
35993695
{
36003696
/* Repoint screens[0] and the renderer's cached top-left
@@ -3613,11 +3709,11 @@ dbool I_StartDisplay(void)
36133709
* to copy into the differently-strided destination
36143710
* itself.
36153711
*
3616-
* Format must be RGB565 -- frontends are allowed to
3617-
* return a different format than SET_PIXEL_FORMAT
3618-
* negotiated, e.g. when running with a HW backend that
3619-
* needs an internal conversion stage. We can only
3620-
* direct-render when the formats agree. */
3712+
* Format must match the one we negotiated -- frontends
3713+
* are allowed to return a different format than
3714+
* SET_PIXEL_FORMAT settled on, e.g. when running with a
3715+
* HW backend that needs an internal conversion stage.
3716+
* We can only direct-render when the formats agree. */
36213717
direct_fb_data = (unsigned char *)fb.data;
36223718
direct_fb_pitch = (unsigned int)fb.pitch;
36233719
screens[0].data = direct_fb_data;
@@ -3634,6 +3730,7 @@ dbool I_StartDisplay(void)
36343730
have_sw_fb = true;
36353731
}
36363732
}
3733+
}
36373734
}
36383735

36393736
return true;

libretro/libretro_core_options.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,21 @@ struct retro_core_option_v2_definition option_defs_us[] = {
6161
},
6262
"320x200"
6363
},
64+
{
65+
"prboom-color_format",
66+
"Color Format (Restart Required)",
67+
NULL,
68+
"Output colour depth. '16bits' is the classic RGB565 renderer. '24bits' renders in full 8-bit-per-channel truecolor, which removes the banding the 16-bit light ramp introduces in distance shading and smooth gradients. '30bits' renders at 10 bits per channel for finer gradients still; it is used only when the frontend can actually present a 10-bit surface, otherwise the core falls back to 24bits automatically.",
69+
NULL,
70+
NULL,
71+
{
72+
{ "16bits", NULL },
73+
{ "24bits (truecolor)", NULL },
74+
{ "30bits (HDR)", NULL },
75+
{ NULL, NULL },
76+
},
77+
"16bits"
78+
},
6479
{
6580
"prboom-mouse_on",
6681
"Mouse Active When Using Gamepad",

src/config.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,14 @@
1010
#include <compat/msvc.h>
1111
#endif
1212

13-
#define SURFACE_PIXEL_DEPTH 2
13+
/* Bytes per surface pixel. Runtime, not a constant: the "Color Format"
14+
* core option selects RGB565 (2) or a 32-bit truecolor format (4) once at
15+
* load, before any surface is allocated. vid_pixelbytes lives in
16+
* vid_mode.c and stays 2 until the libretro layer negotiates otherwise, so
17+
* every consumer that predates the option behaves exactly as before.
18+
* Only used in ordinary expressions -- never in preprocessor arithmetic. */
19+
extern int vid_pixelbytes;
20+
#define SURFACE_PIXEL_DEPTH vid_pixelbytes
1421
extern int SCREENWIDTH;
1522
extern int SCREENHEIGHT;
1623
extern int SCREENPITCH;

src/f_wipe.c

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,14 @@ static int wipe_doMelt(int ticks)
135135
* screen rather than relying on previous frames' writes. */
136136
for (y = 0; y < SCREENHEIGHT; y++)
137137
{
138-
uint16_t *drow =
139-
(uint16_t *)(wipe_scr.data + (size_t)y * SURFACE_BYTE_PITCH);
140-
const uint16_t *erow =
141-
(const uint16_t *)(wipe_scr_end.data + (size_t)y * SURFACE_BYTE_PITCH);
138+
/* Byte-addressed: the melt is a pure copy with no colour maths,
139+
* so the same loop serves every surface width -- pixel offsets
140+
* just scale by SURFACE_PIXEL_DEPTH. Nothing here converts
141+
* between formats. */
142+
uint8_t *drow =
143+
wipe_scr.data + (size_t)y * SURFACE_BYTE_PITCH;
144+
const uint8_t *erow =
145+
wipe_scr_end.data + (size_t)y * SURFACE_BYTE_PITCH;
142146

143147
i = 0;
144148
while (i < SCREENWIDTH)
@@ -157,18 +161,21 @@ static int wipe_doMelt(int ticks)
157161
break;
158162
run++;
159163
}
160-
memcpy(&drow[i], &erow[i],
164+
memcpy(drow + (size_t)i * SURFACE_PIXEL_DEPTH,
165+
erow + (size_t)i * SURFACE_PIXEL_DEPTH,
161166
(size_t)(run - i) * SURFACE_PIXEL_DEPTH);
162167
i = run;
163168
}
164169
else
165170
{
166171
/* Start-screen pixel, scrolled down by the boundary:
167172
* dest row y reads source row y - b. */
168-
const uint16_t *srow =
169-
(const uint16_t *)(wipe_scr_start.data
170-
+ (size_t)(y - b) * SURFACE_BYTE_PITCH);
171-
drow[i] = srow[i];
173+
const uint8_t *srow =
174+
wipe_scr_start.data
175+
+ (size_t)(y - b) * SURFACE_BYTE_PITCH;
176+
memcpy(drow + (size_t)i * SURFACE_PIXEL_DEPTH,
177+
srow + (size_t)i * SURFACE_PIXEL_DEPTH,
178+
SURFACE_PIXEL_DEPTH);
172179
i++;
173180
}
174181
}

0 commit comments

Comments
 (0)