diff --git a/src/Graphics_Dreamcast.c b/src/Graphics_Dreamcast.c new file mode 100644 index 000000000..cb7c811ff --- /dev/null +++ b/src/Graphics_Dreamcast.c @@ -0,0 +1,432 @@ +#include "Core.h" +#if defined CC_BUILD_DREAMCAST +#include "_GraphicsBase.h" +#include "Errors.h" +#include "Logger.h" +#include "Window.h" +#include "GL/gl.h" +#include "GL/glkos.h" +#include "GL/glext.h" +#include +#define PIXEL_FORMAT GL_RGBA + +#define TRANSFER_FORMAT GL_UNSIGNED_BYTE +/* Current format and size of vertices */ +static int gfx_stride, gfx_format = -1; + + +/*########################################################################################################################* +*---------------------------------------------------------General---------------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_Create(void) { + glKosInit(); + glGetIntegerv(GL_MAX_TEXTURE_SIZE, &Gfx.MaxTexWidth); + Gfx.MaxTexHeight = Gfx.MaxTexWidth; + Gfx.Created = true; + Gfx_RestoreState(); +} + +cc_bool Gfx_TryRestoreContext(void) { + return true; +} + +void Gfx_Free(void) { + Gfx_FreeState(); +} + +#define gl_Toggle(cap) if (enabled) { glEnable(cap); } else { glDisable(cap); } + + +/*########################################################################################################################* +*-----------------------------------------------------State management----------------------------------------------------* +*#########################################################################################################################*/ +static PackedCol gfx_clearColor; +void Gfx_SetFaceCulling(cc_bool enabled) { gl_Toggle(GL_CULL_FACE); } +void Gfx_SetAlphaBlending(cc_bool enabled) { gl_Toggle(GL_BLEND); } +void Gfx_SetAlphaArgBlend(cc_bool enabled) { } + +static void GL_ClearColor(PackedCol color) { + glClearColor(PackedCol_R(color) / 255.0f, PackedCol_G(color) / 255.0f, + PackedCol_B(color) / 255.0f, PackedCol_A(color) / 255.0f); +} +void Gfx_ClearCol(PackedCol color) { + if (color == gfx_clearColor) return; + GL_ClearColor(color); + gfx_clearColor = color; +} + +void Gfx_SetColWriteMask(cc_bool r, cc_bool g, cc_bool b, cc_bool a) { + glColorMask(r, g, b, a); +} + +void Gfx_SetDepthWrite(cc_bool enabled) { glDepthMask(enabled); } +void Gfx_SetDepthTest(cc_bool enabled) { gl_Toggle(GL_DEPTH_TEST); } + +void Gfx_SetTexturing(cc_bool enabled) { } + +void Gfx_SetAlphaTest(cc_bool enabled) { gl_Toggle(GL_ALPHA_TEST); } + +void Gfx_DepthOnlyRendering(cc_bool depthOnly) { + cc_bool enabled = !depthOnly; + Gfx_SetColWriteMask(enabled, enabled, enabled, enabled); + gl_Toggle(GL_TEXTURE_2D); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Matrices--------------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_CalcOrthoMatrix(struct Matrix* matrix, float width, float height, float zNear, float zFar) { + /* Transposed, source https://learn.microsoft.com/en-us/windows/win32/opengl/glortho */ + /* The simplified calculation below uses: L = 0, R = width, T = 0, B = height */ + *matrix = Matrix_Identity; + + matrix->row1.X = 2.0f / width; + matrix->row2.Y = -2.0f / height; + matrix->row3.Z = -2.0f / (zFar - zNear); + + matrix->row4.X = -1.0f; + matrix->row4.Y = 1.0f; + matrix->row4.Z = -(zFar + zNear) / (zFar - zNear); +} + +static double Cotangent(double x) { return Math_Cos(x) / Math_Sin(x); } +void Gfx_CalcPerspectiveMatrix(struct Matrix* matrix, float fov, float aspect, float zFar) { + float zNear = 0.1f; + float c = (float)Cotangent(0.5f * fov); + + /* Transposed, source https://learn.microsoft.com/en-us/windows/win32/opengl/glfrustum */ + /* For a FOV based perspective matrix, left/right/top/bottom are calculated as: */ + /* left = -c * aspect, right = c * aspect, bottom = -c, top = c */ + /* Calculations are simplified because of left/right and top/bottom symmetry */ + *matrix = Matrix_Identity; + + matrix->row1.X = c / aspect; + matrix->row2.Y = c; + matrix->row3.Z = -(zFar + zNear) / (zFar - zNear); + matrix->row3.W = -1.0f; + matrix->row4.Z = -(2.0f * zFar * zNear) / (zFar - zNear); + matrix->row4.W = 0.0f; +} + + +/*########################################################################################################################* +*-----------------------------------------------------------Misc----------------------------------------------------------* +*#########################################################################################################################*/ +cc_result Gfx_TakeScreenshot(struct Stream* output) { + return ERR_NOT_SUPPORTED; +} + +void Gfx_GetApiInfo(cc_string* info) { + int pointerSize = sizeof(void*) * 8; + + String_Format1(info, "-- Using OpenGL (%i bit) --\n", &pointerSize); + String_Format1(info, "Vendor: %c\n", glGetString(GL_VENDOR)); + String_Format1(info, "Renderer: %c\n", glGetString(GL_RENDERER)); + String_Format1(info, "GL version: %c\n", glGetString(GL_VERSION)); + String_Format2(info, "Max texture size: (%i, %i)\n", &Gfx.MaxTexWidth, &Gfx.MaxTexHeight); +} + +void Gfx_SetFpsLimit(cc_bool vsync, float minFrameMs) { + gfx_minFrameMs = minFrameMs; + gfx_vsync = vsync; +} + +void Gfx_BeginFrame(void) { } +void Gfx_Clear(void) { + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); +} + +void Gfx_EndFrame(void) { + glKosSwapBuffers(); + if (gfx_minFrameMs) LimitFPS(); +} + +void Gfx_OnWindowResize(void) { + /* TODO: Eliminate this nasty hack.. */ + Game_UpdateDimensions(); + glViewport(0, 0, Game.Width, Game.Height); +} + + +/*########################################################################################################################* +*-------------------------------------------------------Index buffers-----------------------------------------------------* +*#########################################################################################################################*/ +static cc_uint16 __attribute__((aligned(16))) gfx_indices[GFX_MAX_INDICES]; +static void* gfx_vertices; +static int vb_size; + +GfxResourceID Gfx_CreateIb2(int count, Gfx_FillIBFunc fillFunc, void* obj) { + fillFunc(gfx_indices, count, obj); +} + +void Gfx_BindIb(GfxResourceID ib) { } +void Gfx_DeleteIb(GfxResourceID* ib) { } + + +/*########################################################################################################################* +*------------------------------------------------------Vertex buffers-----------------------------------------------------* +*#########################################################################################################################*/ +GfxResourceID Gfx_CreateVb(VertexFormat fmt, int count) { + void* data = memalign(16, count * strideSizes[fmt]); + if (!data) Logger_Abort("Failed to allocate memory for GFX VB"); + return data; + //return Mem_Alloc(count, strideSizes[fmt], "gfx VB"); +} + +void Gfx_BindVb(GfxResourceID vb) { gfx_vertices = vb; } + +void Gfx_DeleteVb(GfxResourceID* vb) { + GfxResourceID data = *vb; + if (data) Mem_Free(data); + *vb = 0; +} + +void* Gfx_LockVb(GfxResourceID vb, VertexFormat fmt, int count) { + vb_size = count * strideSizes[fmt]; + return vb; +} + +void Gfx_UnlockVb(GfxResourceID vb) { + gfx_vertices = vb; + //sceKernelDcacheWritebackInvalidateRange(vb, vb_size); +} + + +GfxResourceID Gfx_CreateDynamicVb(VertexFormat fmt, int maxVertices) { + void* data = memalign(16, maxVertices * strideSizes[fmt]); + if (!data) Logger_Abort("Failed to allocate memory for GFX VB"); + return data; + //return Mem_Alloc(maxVertices, strideSizes[fmt], "gfx VB"); +} + +void* Gfx_LockDynamicVb(GfxResourceID vb, VertexFormat fmt, int count) { + vb_size = count * strideSizes[fmt]; + return vb; +} + +void Gfx_UnlockDynamicVb(GfxResourceID vb) { + gfx_vertices = vb; + //dcache_flush_range(vb, vb_size); +} + +void Gfx_SetDynamicVbData(GfxResourceID vb, void* vertices, int vCount) { + gfx_vertices = vb; + Mem_Copy(vb, vertices, vCount * gfx_stride); + //dcache_flush_range(vertices, vCount * gfx_stride); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Textures--------------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_BindTexture(GfxResourceID texId) { + int tex = texId; + glBindTexture(GL_TEXTURE_2D, (GLuint)texId); +} + +GfxResourceID Gfx_CreateTexture(struct Bitmap* bmp, cc_uint8 flags, cc_bool mipmaps) { + GLuint texId; + glGenTextures(1, &texId); + glBindTexture(GL_TEXTURE_2D, texId); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + + if (!Math_IsPowOf2(bmp->width) || !Math_IsPowOf2(bmp->height)) { + Logger_Abort("Textures must have power of two dimensions"); + } + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, bmp->width, bmp->height, 0, PIXEL_FORMAT, GL_UNSIGNED_BYTE, bmp->scan0); + + return texId; +} + +#define UPDATE_FAST_SIZE (64 * 64) +static CC_NOINLINE void UpdateTextureSlow(int x, int y, struct Bitmap* part, int rowWidth) { + BitmapCol buffer[UPDATE_FAST_SIZE]; + void* ptr = (void*)buffer; + int count = part->width * part->height; + + /* cannot allocate memory on the stack for very big updates */ + if (count > UPDATE_FAST_SIZE) { + ptr = Mem_Alloc(count, 4, "Gfx_UpdateTexture temp"); + } + + CopyTextureData(ptr, part->width << 2, part, rowWidth << 2); + glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, part->width, part->height, PIXEL_FORMAT, GL_UNSIGNED_BYTE, ptr); + if (count > UPDATE_FAST_SIZE) Mem_Free(ptr); +} + +void Gfx_UpdateTexture(GfxResourceID texId, int x, int y, struct Bitmap* part, int rowWidth, cc_bool mipmaps) { + glBindTexture(GL_TEXTURE_2D, (GLuint)texId); + /* TODO: Use GL_UNPACK_ROW_LENGTH for Desktop OpenGL */ + + if (part->width == rowWidth) { + glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, part->width, part->height, PIXEL_FORMAT, GL_UNSIGNED_BYTE, part->scan0); + } else { + UpdateTextureSlow(x, y, part, rowWidth); + } +} + +void Gfx_UpdateTexturePart(GfxResourceID texId, int x, int y, struct Bitmap* part, cc_bool mipmaps) { + Gfx_UpdateTexture(texId, x, y, part, part->width, mipmaps); +} + +void Gfx_DeleteTexture(GfxResourceID* texId) { + GLuint id = (GLuint)(*texId); + if (!id) return; + glDeleteTextures(1, &id); + *texId = 0; +} + +void Gfx_EnableMipmaps(void) { } +void Gfx_DisableMipmaps(void) { } + + +/*########################################################################################################################* +*-----------------------------------------------------State management----------------------------------------------------* +*#########################################################################################################################*/ +static PackedCol gfx_fogColor; +static float gfx_fogEnd = -1.0f, gfx_fogDensity = -1.0f; +static int gfx_fogMode = -1; + +void Gfx_SetFog(cc_bool enabled) { + gfx_fogEnabled = enabled; + if (enabled) { glEnable(GL_FOG); } else { glDisable(GL_FOG); } +} + +void Gfx_SetFogCol(PackedCol color) { + float rgba[4]; + if (color == gfx_fogColor) return; + + rgba[0] = PackedCol_R(color) / 255.0f; + rgba[1] = PackedCol_G(color) / 255.0f; + rgba[2] = PackedCol_B(color) / 255.0f; + rgba[3] = PackedCol_A(color) / 255.0f; + + glFogfv(GL_FOG_COLOR, rgba); + gfx_fogColor = color; +} + +void Gfx_SetFogDensity(float value) { + if (value == gfx_fogDensity) return; + glFogf(GL_FOG_DENSITY, value); + gfx_fogDensity = value; +} + +void Gfx_SetFogEnd(float value) { + if (value == gfx_fogEnd) return; + glFogf(GL_FOG_END, value); + gfx_fogEnd = value; +} + +void Gfx_SetFogMode(FogFunc func) { + static GLint modes[3] = { GL_LINEAR, GL_EXP, GL_EXP2 }; + if (func == gfx_fogMode) return; + + glFogi(GL_FOG_MODE, modes[func]); + gfx_fogMode = func; +} + + +/*########################################################################################################################* +*---------------------------------------------------------Matrices--------------------------------------------------------* +*#########################################################################################################################*/ +static GLenum matrix_modes[] = { GL_PROJECTION, GL_MODELVIEW, GL_TEXTURE }; +static int lastMatrix; + +void Gfx_LoadMatrix(MatrixType type, const struct Matrix* matrix) { + if (type != lastMatrix) { lastMatrix = type; glMatrixMode(matrix_modes[type]); } + glLoadMatrixf((const float*)matrix); +} + +void Gfx_LoadIdentityMatrix(MatrixType type) { + if (type != lastMatrix) { lastMatrix = type; glMatrixMode(matrix_modes[type]); } + glLoadIdentity(); +} + +static struct Matrix texMatrix = Matrix_IdentityValue; +void Gfx_EnableTextureOffset(float x, float y) { + texMatrix.row4.X = x; texMatrix.row4.Y = y; + Gfx_LoadMatrix(2, &texMatrix); +} + +void Gfx_DisableTextureOffset(void) { Gfx_LoadIdentityMatrix(2); } + + +/*########################################################################################################################* +*-------------------------------------------------------State setup-------------------------------------------------------* +*#########################################################################################################################*/ +static void Gfx_FreeState(void) { FreeDefaultResources(); } +static void Gfx_RestoreState(void) { + InitDefaultResources(); + glEnableClientState(GL_VERTEX_ARRAY); + glEnableClientState(GL_COLOR_ARRAY); + gfx_format = -1; + + glHint(GL_FOG_HINT, GL_NICEST); + glAlphaFunc(GL_GREATER, 0.5f); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glDepthFunc(GL_LEQUAL); +} + +cc_bool Gfx_WarnIfNecessary(void) { + return false; +} + + +/*########################################################################################################################* +*----------------------------------------------------------Drawing--------------------------------------------------------* +*#########################################################################################################################*/ +#define VB_PTR gfx_vertices + +static void SetupVertices(int startVertex) { + if (gfx_format == VERTEX_FORMAT_TEXTURED) { + cc_uint32 offset = startVertex * SIZEOF_VERTEX_TEXTURED; + glVertexPointer(3, GL_FLOAT, SIZEOF_VERTEX_TEXTURED, (void*)(VB_PTR + offset)); + glColorPointer(4, GL_UNSIGNED_BYTE, SIZEOF_VERTEX_TEXTURED, (void*)(VB_PTR + offset + 12)); + glTexCoordPointer(2, GL_FLOAT, SIZEOF_VERTEX_TEXTURED, (void*)(VB_PTR + offset + 16)); + } else { + cc_uint32 offset = startVertex * SIZEOF_VERTEX_COLOURED; + glVertexPointer(3, GL_FLOAT, SIZEOF_VERTEX_COLOURED, (void*)(VB_PTR + offset)); + glColorPointer(4, GL_UNSIGNED_BYTE, SIZEOF_VERTEX_COLOURED, (void*)(VB_PTR + offset + 12)); + } +} + +void Gfx_SetVertexFormat(VertexFormat fmt) { + if (fmt == gfx_format) return; + gfx_format = fmt; + gfx_stride = strideSizes[fmt]; + + if (fmt == VERTEX_FORMAT_TEXTURED) { + glEnableClientState(GL_TEXTURE_COORD_ARRAY); + glEnable(GL_TEXTURE_2D); + } else { + glDisableClientState(GL_TEXTURE_COORD_ARRAY); + glDisable(GL_TEXTURE_2D); + } +} + +void Gfx_DrawVb_Lines(int verticesCount) { + SetupVertices(0); + glDrawArrays(GL_LINES, 0, verticesCount); +} + +void Gfx_DrawVb_IndexedTris_Range(int verticesCount, int startVertex) { + SetupVertices(startVertex); + glDrawElements(GL_TRIANGLES, ICOUNT(verticesCount), GL_UNSIGNED_SHORT, gfx_indices); +} + +void Gfx_DrawVb_IndexedTris(int verticesCount) { + SetupVertices(0); + glDrawElements(GL_TRIANGLES, ICOUNT(verticesCount), GL_UNSIGNED_SHORT, gfx_indices); +} + +void Gfx_DrawIndexedTris_T2fC4b(int verticesCount, int startVertex) { + cc_uint32 offset = startVertex * SIZEOF_VERTEX_TEXTURED; + glVertexPointer(3, GL_FLOAT, SIZEOF_VERTEX_TEXTURED, (void*)(VB_PTR + offset)); + glColorPointer(4, GL_UNSIGNED_BYTE, SIZEOF_VERTEX_TEXTURED, (void*)(VB_PTR + offset + 12)); + glTexCoordPointer(2, GL_FLOAT, SIZEOF_VERTEX_TEXTURED, (void*)(VB_PTR + offset + 16)); + glDrawElements(GL_TRIANGLES, ICOUNT(verticesCount), GL_UNSIGNED_SHORT, gfx_indices); +} +#endif \ No newline at end of file diff --git a/src/Platform_Dreamcast.c b/src/Platform_Dreamcast.c index dee289c85..fe39d42f2 100644 --- a/src/Platform_Dreamcast.c +++ b/src/Platform_Dreamcast.c @@ -229,19 +229,21 @@ static void* ExecThread(void* param) { } void* Thread_Create(Thread_StartFunc func) { - // TODO: Probably wrong to use 0 here.... - return NULL;//return thd_create(0, ExecThread, func); + kthread_attr_t attrs = { 0 }; + attrs.stack_size = 64 * 1024; + attrs.label = "CC thread"; + return thd_create_ex(&attrs, ExecThread, func); } void Thread_Start2(void* handle, Thread_StartFunc func) { } void Thread_Detach(void* handle) { - //thd_detach((kthread_t*)handle); + thd_detach((kthread_t*)handle); } void Thread_Join(void* handle) { - //thd_join((kthread_t*)handle, NULL); + thd_join((kthread_t*)handle, NULL); } void* Mutex_Create(void) { @@ -540,6 +542,10 @@ cc_bool DynamicLib_DescribeError(cc_string* dst) { void Platform_Init(void) { Platform_SingleProcess = true; /*pspDebugSioInit();*/ + + char cwd[600] = { 0 }; + char* ptr = getcwd(cwd, 600); + Platform_Log1("WORKING DIR: %c", ptr); } void Platform_Free(void) { } diff --git a/src/Program.c b/src/Program.c index 909d66c1c..7e40eda0d 100644 --- a/src/Program.c +++ b/src/Program.c @@ -138,7 +138,7 @@ void android_main(void) { SetupProgram(0, NULL); for (;;) { RunProgram(0, NULL); } } -#elif defined CC_BUILD_3DS || defined CC_BUILD_PSP || defined CC_BUILD_GCWII +#elif defined CC_BUILD_3DS || defined CC_BUILD_PSP || defined CC_BUILD_GCWII || defined CC_BUILD_DREAMCAST int main(int argc, char** argv) { SetupProgram(argc, argv); while (WindowInfo.Exists) { @@ -147,7 +147,7 @@ int main(int argc, char** argv) { return 0; } #else -/* NOTE: main_real is used for when compiling with MingW without linking to startup files. */ +/* NOTE: main_real is used for when compiling with MinGW without linking to startup files. */ /* Normally, the final code produced for "main" is our "main" combined with crt's main */ /* (mingw-w64-crt/crt/gccmain.c) - alas this immediately crashes the game on startup. */ /* Using main_real instead and setting main_real as the entrypoint fixes the crash. */ diff --git a/src/Window_Dreamast.c b/src/Window_Dreamast.c index d58455227..37bc08942 100644 --- a/src/Window_Dreamast.c +++ b/src/Window_Dreamast.c @@ -57,11 +57,17 @@ void Window_Close(void) { /*########################################################################################################################* *----------------------------------------------------Input processing-----------------------------------------------------* *#########################################################################################################################*/ +static void HandleButtons_Launcher(int mods) { + Input_SetNonRepeatable(CCPAD_START, mods & CONT_A); + Input_SetNonRepeatable(CCPAD_SELECT, mods & CONT_B); + + Input_SetNonRepeatable(CCPAD_LEFT, mods & CONT_DPAD_LEFT); + Input_SetNonRepeatable(CCPAD_RIGHT, mods & CONT_DPAD_RIGHT); + Input_SetNonRepeatable(CCPAD_UP, mods & CONT_DPAD_UP); + Input_SetNonRepeatable(CCPAD_DOWN, mods & CONT_DPAD_DOWN); +} static void HandleButtons_Game(int mods) { - // TODO: https://github.com/KallistiOS/KallistiOS/blob/90e09d81d7c1f9dc3f31290a8fff94e4d5ff304a/kernel/arch/dreamcast/include/dc/maple/controller.h#L41 - //Input_SetNonRepeatable(CCPAD_L, mods & CONT_L); - //Input_SetNonRepeatable(CCPAD_R, mods & CONT_R); - // TODO CONT_Z, joysticks + // TODO CONT_Z Input_SetNonRepeatable(CCPAD_A, mods & CONT_A); Input_SetNonRepeatable(CCPAD_B, mods & CONT_B); @@ -76,14 +82,19 @@ static void HandleButtons_Game(int mods) { Input_SetNonRepeatable(CCPAD_UP, mods & CONT_DPAD_UP); Input_SetNonRepeatable(CCPAD_DOWN, mods & CONT_DPAD_DOWN); } -static void HandleButtons_Launcher(int mods) { - Input_SetNonRepeatable(CCPAD_START, mods & CONT_A); - Input_SetNonRepeatable(CCPAD_SELECT, mods & CONT_B); - - Input_SetNonRepeatable(CCPAD_LEFT, mods & CONT_DPAD_LEFT); - Input_SetNonRepeatable(CCPAD_RIGHT, mods & CONT_DPAD_RIGHT); - Input_SetNonRepeatable(CCPAD_UP, mods & CONT_DPAD_UP); - Input_SetNonRepeatable(CCPAD_DOWN, mods & CONT_DPAD_DOWN); +static void HandleController(cont_state_t* state) { + Input_SetNonRepeatable(CCPAD_L, state->ltrig > 10); + Input_SetNonRepeatable(CCPAD_R, state->rtrig > 10); + // TODO CONT_Z, joysticks + // TODO: verify values are write + + if (Input.RawMode) { + int dx = state->joyx, dy = state->joyy; + if (Math_AbsI(dx) <= 8) dx = 0; + if (Math_AbsI(dy) <= 8) dy = 0; + + Event_RaiseRawMove(&PointerEvents.RawMoved, dx / 8.0f, dy / 8.0f); + } } static void ProcessControllerInput(void) { @@ -100,7 +111,9 @@ static void ProcessControllerInput(void) { HandleButtons_Launcher(mods); } else { HandleButtons_Game(mods); + HandleController(state); } + } void Window_ProcessEvents(double delta) { @@ -128,6 +141,10 @@ void Window_DrawFramebuffer(Rect2D r) { // TODO: Don't redraw everything int size = fb_bmp.width * fb_bmp.height * 4; + cc_uint32* row = Bitmap_GetRow(&fb_bmp, 20); + bfont_draw_str_ex(row, fb_bmp.width, 0xFFEEDDCC, 0x55667788, 32, + true, "ABC III LLL"); + // TODO: double buffering ?? // https://dcemulation.org/phpBB/viewtopic.php?t=99999 // https://dcemulation.org/phpBB/viewtopic.php?t=43214