From 0d1179808f4b3a043c66b9ea6027f2f4f70fb98a Mon Sep 17 00:00:00 2001 From: GPF Date: Fri, 5 Jun 2026 20:49:35 -0700 Subject: [PATCH] Add nehe06_dt Dreamcast texture sample Add a Dreamcast GLdc sample that loads a .dt texture from romdisk. This adds a new nehe06_dt sample based on the existing NeHe cube demo, but loading a Dreamcast .dt texture directly from romdisk instead of the older texture formats. Changes: - add samples/nehe06_dt/main.c - add samples/nehe06_dt/romdisk/NeHe.dt - register the sample in CMakeLists.txt The sample is useful for checking GLdc texture loading with Dreamcast-native .dt assets and for comparing the output against the source artwork on hardware. --- CMakeLists.txt | 1 + samples/nehe06_dt/main.c | 339 ++++++++++++++++++++++++++++++ samples/nehe06_dt/romdisk/NeHe.dt | Bin 0 -> 10272 bytes 3 files changed, 340 insertions(+) create mode 100644 samples/nehe06_dt/main.c create mode 100644 samples/nehe06_dt/romdisk/NeHe.dt diff --git a/CMakeLists.txt b/CMakeLists.txt index 06ed35c..28d1a34 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -202,6 +202,7 @@ if(BUILD_SAMPLES) gen_sample(nehe05 samples/nehe05/main.c) gen_sample(nehe06 samples/nehe06/main.c samples/loadbmp.c) gen_sample(nehe06_vq samples/nehe06_vq/main.c) + gen_sample(nehe06_dt samples/nehe06_dt/main.c) gen_sample(nehe06_4444twid samples/nehe06_4444twid/main.c) gen_sample(nehe08 samples/nehe08/main.c samples/nehe08/pvr-texture.c) gen_sample(nehe10 samples/nehe10/main.c samples/loadbmp.c) diff --git a/samples/nehe06_dt/main.c b/samples/nehe06_dt/main.c new file mode 100644 index 0000000..e80ea00 --- /dev/null +++ b/samples/nehe06_dt/main.c @@ -0,0 +1,339 @@ +#include +#include +#include +#include + +#ifdef _arch_dreamcast +#include +#endif + +#include "GL/gl.h" +#include "GL/glu.h" +#include "GL/glkos.h" +#include "GL/glext.h" + +#ifdef _arch_dreamcast +extern uint8 romdisk[]; +KOS_INIT_ROMDISK(romdisk); +// /opt/toolchains/dc/kos/utils/pvrtex/pvrtex -i romdisk/NeHe.bmp -f rgb565 -c -o romdisk/NeHe.dt +#define IMG_PATH "/rd/NeHe.dt" +#else +#define IMG_PATH "../samples/nehe06_dt/romdisk/NeHe.dt" +#endif + +/* floats for x rotation, y rotation, z rotation */ +float xrot, yrot, zrot; + +/* storage for one texture */ +GLuint texture[1]; + +typedef struct __attribute__((packed)) DcTxHeader { + char fourcc[4]; + uint32_t chunk_size; + uint8_t version; + uint8_t header_size; + uint8_t codebook_size; + uint8_t colors_used; + uint16_t width_pixels; + uint16_t height_pixels; + uint32_t pvr_type; + uint32_t pad1; + uint32_t pad2; + uint32_t pad3; +} DcTxHeader; + +typedef struct Image { + unsigned long sizeX; + unsigned long sizeY; + void *buffer; + void *data; + GLenum format; + GLenum internal_format; + GLenum type; + GLboolean compressed; + unsigned int dataSize; +} Image; + +static int ImageLoad(const char *filename, Image *image) { + FILE *file = NULL; + long file_size; + DcTxHeader *header; + size_t header_size; + + file = fopen(filename, "rb"); + if(!file) { + printf("File Not Found : %s\n", filename); + return 0; + } + + if(fseek(file, 0, SEEK_END) != 0) { + fclose(file); + return 0; + } + + file_size = ftell(file); + if(file_size <= 0) { + fclose(file); + return 0; + } + + rewind(file); + + image->buffer = malloc((size_t)file_size); + if(!image->buffer) { + fclose(file); + return 0; + } + + if(fread(image->buffer, (size_t)file_size, 1, file) != 1) { + fclose(file); + free(image->buffer); + image->buffer = NULL; + return 0; + } + + fclose(file); + + if((size_t)file_size < sizeof(DcTxHeader)) { + free(image->buffer); + image->buffer = NULL; + return 0; + } + + header = (DcTxHeader *)image->buffer; + if(memcmp(header->fourcc, "DcTx", 4) != 0) { + printf("Invalid .dt file header: %s\n", filename); + free(image->buffer); + image->buffer = NULL; + return 0; + } + + header_size = (size_t)(header->header_size + 1) * 32; + if(header_size > (size_t)file_size || header->chunk_size > (uint32_t)file_size) { + printf("Invalid .dt file size: %s\n", filename); + free(image->buffer); + image->buffer = NULL; + return 0; + } + + image->sizeX = header->width_pixels; + image->sizeY = header->height_pixels; + image->data = (void *)((uint8_t *)image->buffer + header_size); + image->dataSize = (unsigned int)(header->chunk_size - header_size); + image->compressed = (header->pvr_type & (1u << 30)) != 0; + + { + GLboolean twiddled = ((header->pvr_type & (1u << 26)) == 0); + GLboolean mipmapped = ((int32_t)header->pvr_type) < 0; + GLuint format = (header->pvr_type >> 27) & 0x7; + + if(image->compressed) { + switch(format) { + case 0: + image->internal_format = mipmapped + ? (twiddled ? GL_COMPRESSED_ARGB_1555_VQ_MIPMAP_TWID_KOS : GL_COMPRESSED_ARGB_1555_VQ_MIPMAP_KOS) + : (twiddled ? GL_COMPRESSED_ARGB_1555_VQ_TWID_KOS : GL_COMPRESSED_ARGB_1555_VQ_KOS); + break; + case 1: + image->internal_format = mipmapped + ? (twiddled ? GL_COMPRESSED_RGB_565_VQ_MIPMAP_TWID_KOS : GL_COMPRESSED_RGB_565_VQ_MIPMAP_KOS) + : (twiddled ? GL_COMPRESSED_RGB_565_VQ_TWID_KOS : GL_COMPRESSED_RGB_565_VQ_KOS); + break; + case 2: + image->internal_format = mipmapped + ? (twiddled ? GL_COMPRESSED_ARGB_4444_VQ_MIPMAP_TWID_KOS : GL_COMPRESSED_ARGB_4444_VQ_MIPMAP_KOS) + : (twiddled ? GL_COMPRESSED_ARGB_4444_VQ_TWID_KOS : GL_COMPRESSED_ARGB_4444_VQ_KOS); + break; + default: + printf("Unsupported compressed texture format in .dt: %u\n", format); + free(image->buffer); + image->buffer = NULL; + return 0; + } + } else { + switch(format) { + case 0: + image->format = GL_BGRA; + image->internal_format = GL_RGBA; + image->type = twiddled ? GL_UNSIGNED_SHORT_1_5_5_5_REV_TWID_KOS : GL_UNSIGNED_SHORT_1_5_5_5_REV; + break; + case 1: + image->format = GL_RGB; + image->internal_format = GL_RGB; + image->type = twiddled ? GL_UNSIGNED_SHORT_5_6_5_TWID_KOS : GL_UNSIGNED_SHORT_5_6_5; + break; + case 2: + image->format = GL_BGRA; + image->internal_format = GL_RGBA; + image->type = twiddled ? GL_UNSIGNED_SHORT_4_4_4_4_REV_TWID_KOS : GL_UNSIGNED_SHORT_4_4_4_4_REV; + break; + default: + printf("Unsupported uncompressed texture format in .dt: %u\n", format); + free(image->buffer); + image->buffer = NULL; + return 0; + } + } + } + + return 1; +} + +/* Load texture from a .dt file */ +static void LoadGLTextures(void) { + Image *image1; + + image1 = (Image *)malloc(sizeof(Image)); + if(image1 == NULL) { + printf("Error allocating space for image\n"); + exit(0); + } + + memset(image1, 0, sizeof(*image1)); + + if(!ImageLoad(IMG_PATH, image1)) { + free(image1); + exit(1); + } + + glGenTextures(1, &texture[0]); + glBindTexture(GL_TEXTURE_2D, texture[0]); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + + if(image1->compressed) { + glCompressedTexImage2DARB( + GL_TEXTURE_2D, 0, image1->internal_format, image1->sizeX, image1->sizeY, 0, + image1->dataSize, image1->data + ); + } else { + glTexImage2D( + GL_TEXTURE_2D, 0, image1->internal_format, image1->sizeX, image1->sizeY, 0, + image1->format, image1->type, image1->data + ); + } + + free(image1->buffer); + free(image1); +} + +/* A general OpenGL initialization function. */ +static void InitGL(int Width, int Height) { + LoadGLTextures(); + glEnable(GL_TEXTURE_2D); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClearDepth(1.0); + glDepthFunc(GL_LESS); + glEnable(GL_DEPTH_TEST); + glShadeModel(GL_SMOOTH); + + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + gluPerspective(45.0f, (GLfloat)Width / (GLfloat)Height, 0.1f, 100.0f); + + glMatrixMode(GL_MODELVIEW); +} + +/* The function called when our window is resized. */ +static void ReSizeGLScene(int Width, int Height) { + if(Height == 0) + Height = 1; + + glViewport(0, 0, Width, Height); + + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + + gluPerspective(45.0f, (GLfloat)Width / (GLfloat)Height, 0.1f, 100.0f); + glMatrixMode(GL_MODELVIEW); +} + +static int check_start(void) { +#ifdef _arch_dreamcast + maple_device_t *cont; + cont_state_t *state; + + cont = maple_enum_type(0, MAPLE_FUNC_CONTROLLER); + if(cont) { + state = (cont_state_t *)maple_dev_status(cont); + if(state) + return state->buttons & CONT_START; + } +#endif + + return 0; +} + +/* The main drawing function. */ +static void DrawGLScene(void) { + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glLoadIdentity(); + + glTranslatef(0.0f, 0.0f, -5.0f); + + glRotatef(xrot, 1.0f, 0.0f, 0.0f); + glRotatef(yrot, 0.0f, 1.0f, 0.0f); + glRotatef(zrot, 0.0f, 0.0f, 1.0f); + + glBindTexture(GL_TEXTURE_2D, texture[0]); + + glBegin(GL_QUADS); + + glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f); + glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f); + glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f); + glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f); + + glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f); + glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f); + glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f); + glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f); + + glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f); + glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, 1.0f, 1.0f); + glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, 1.0f, 1.0f); + glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f); + + glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, -1.0f, -1.0f); + glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, -1.0f, -1.0f); + glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f); + glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f); + + glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f); + glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f); + glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f); + glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f); + + glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f); + glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f); + glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f); + glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f); + + glEnd(); + + xrot += 1.5f; + yrot += 1.5f; + zrot += 1.5f; + + glKosSwapBuffers(); +} + +int main(int argc, char **argv) { + (void)argc; + (void)argv; + + glKosInit(); + + InitGL(640, 480); + ReSizeGLScene(640, 480); + + while(1) { + if(check_start()) + break; + + DrawGLScene(); + } + + return 0; +} diff --git a/samples/nehe06_dt/romdisk/NeHe.dt b/samples/nehe06_dt/romdisk/NeHe.dt new file mode 100644 index 0000000000000000000000000000000000000000..a1aebbb9b751a02342035aabe1e4e58c2877653f GIT binary patch literal 10272 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 literal 0 HcmV?d00001