N64 programming (libdragon)

Discussion of development of software for any "obsolete" computer or video game system. See the WSdev wiki and ObscureDev wiki for more information on certain platforms.
BMBx64
Posts: 25
Joined: Thu May 25, 2017 7:27 am

N64 programming (libdragon)

Post by BMBx64 »

Hi, since i don't know a place for N64 scene im trying here, i was wondering if anyone have done some tests with libdragon.

So far i did some.. well very basic tests, my target is always 60fps, so i use the RDP rasterizer, i have some question on the bottom of the message, sorry for my english and the long post.

These tests works on real hardware, maybe on CEN64 or MESS, some downloads are attached others on mega.

TEST 1: EXPLOSIONS
Random explosions on screen using textures of 16bits (32x32) with RDP.
Image

Performance without optimizations:
410 at 60fps (NTSC)
507 at 50fps (PAL)

Texture strategy, use the same texture for other sprites without reload TMEM:
480
579

Flush texture disabled:
593
728

Probably there must be other ways to optimize further.

DOWNLOAD
https://mega.nz/#!Rkp2jRgJ!GpFxMwJ3g294r1CjwrmYR6mi-nIq4nhMuINdrsJRPwo

TEST 2: BIGGER EXPLOSIONS
Instead of 2KB explosions these are around 122x96 (22KB), so they are split in several pieces to fit the 4KB cache (i dont use stride function on libdragon, is slower than manage single textures), i wonder if there is an easy way to use 4 or 8bit textures with libdragon (they are limited to 16bit-32bit).
Image

Performance is lower than expected, around 50 explosions at 60fps.

DOWNLOAD
https://mega.nz/#!loAQyQSb!NjiU9HUihztLdVz-ULFNcx8NhmLFnQBYuYk5FHTgFhw

TEST 3: SNOW
RDP rectangles (non textured) of 4x4 with a blue color.
Image

Performance:
5120 rectangles at 60fps
6430 rectangles at 50fps

Use int16 instead of int32:
5600
7120

DOWNLOAD
https://mega.nz/#!R9YEwBSD!_7jzjJd0UXLLDIgvKNxVt0pwtKhSdyJ46QhFRMG7EWA

CONTROLS
A - Increase
B - Decrease
Start - Min
Z - Max
C up - clean buffer with RDP (test 1,2)
C left - clean buffer with CPU (test 1,2)
C down - do not clean (test 1,2)
R - Color rectangles (test 3)

CASTLEVANIA: SOTN OST
The whole soundtrack converted into ogg 44KHz/Stereo/128kbps using libogg, 34 songs, fits on a 64MB cartridge, plays from start to end, can skip songs with c button.
Image

It slowdowns 25fps to sync with the audio (i don't know how to use threads), but since its static background there is no problem at all.

DOWNLOAD (wonder if can be shared?)
https://mega.nz/#!Y9pQjYgC!S3LojMylb3lQ-RWEpdtdVxzCJKy9Td1b9AHr7Sq370w

=> Added get_pixel function, this is already inside the libdragon but i didnt see the function for end users?

GET_PIXEL
Gets a pixel color from a buffer in x/y position.
- Returns a packed color (twice) in 16bits for RDP compatibility.

graphics.c:

Code: Select all

uint32_t get_pixel( display_context_t disp, int x, int y )
{
    if( disp == 0 ) { return 0; }

    if( __bitdepth == 2 )
    {
        uint16_t *buffer16 = (uint16_t *)__get_buffer( disp );
	     uint16_t packed_rdp = __get_pixel( buffer16, x, y );
        return packed_rdp | (packed_rdp << 16);
    }
    else
    {
        uint32_t *buffer32 = (uint32_t *)__get_buffer( disp );
        return __get_pixel( buffer32, x, y );
    }
	
}
graphics.h

Code: Select all

uint32_t get_pixel( display_context_t disp, int x, int y);
USES
- Can be used on a paint program (or those racing games with a menu to select a color for a car)
- Can be used as hidden collision map (since it reads the current buffer)
- Allows users to create new functions
- Can transform color into data to copy block areas or do special effects (VERY SLOW)

TEST
A color table is written on the buffer, the mouse reads the color from the buffer and shows it on the rectangle.
Image

16bit value to RGB conversion (this conversion is not necessary, but just in case)

Code: Select all

color=get_pixel(disp,mouse_x,mouse_y);

// Extract
uint8_t r1 = (color & 0xF800) >> 11; // 63488
uint8_t g1 = (color & 0x7C0) >> 6; // 1984
uint8_t b1 = (color & 0x3E) >> 1; // 62

// Expand to 8-bit
r = r1 << 3;
g = g1 << 3;
b = b1 << 3;
Uses RGB555, i was expecting 565.

CONTROLS
Joystick - Mouse
A - Set background color with the color selected
R - Alternate white or black letter fonts
Z - Reset background and letter colors

DOWNLOAD
https://mega.nz/#!g5IRgYwA!Mk-vrgPmAi0HhN5aPKPLNDNVnbtdgxFkGXTkmGBmZcg

Added math functions (fget_angle, fget_dist, get_distx, get_disty)

Allows some game logic and this kind of effects..
Image

Since i don't have yet raster effects (buffer) i had to use giant horizontally textures (320x4), so i can move the blocks separately.

- Libdragon won't allow textures beyond 256 wide, they are repeated at this point or mirrored depending on the settings.

The effect is generated in real time (instead of tables) and can be edited:
C left / C right = wave
C up / C down = radius
A / B = Speed
R = hide text

A better gif (graphics sample from Last Blade 2):
Image

DOWNLOAD
https://mega.nz/#!UpIBkJBb!TqZc9F3V8lZS ... 9zsYVEpews

---
I could provide the source code of these examples, but probably the level skills on this board are better than me.

Right now im trying to figure how to add hardware flip to the sprites, it is possible by changing the order of the loading or using mirror S or T coords when drawing, but this code brings me headaches, i simply don't know where to start:

(the code is slightly modified from the source, i have removed texture slots)

rdp.c

Code: Select all

static uint32_t __rdp_load_texture(int mirror_enabled, sprite_t *sprite, int sl, int tl, int sh, int th )
{
    /* Invalidate data associated with sprite in cache */
    if( flush_strategy == FLUSH_STRATEGY_AUTOMATIC )
    {
        data_cache_hit_writeback_invalidate( sprite->data, sprite->width * sprite->height * sprite->bitdepth );
    }

    /* Point the RDP at the actual sprite data */
    __rdp_ringbuffer_queue( 0xFD000000 | ((sprite->bitdepth == 2) ? 0x00100000 : 0x00180000) | (sprite->width - 1) );
    __rdp_ringbuffer_queue( (uint32_t)sprite->data );
    __rdp_ringbuffer_send();

    /* Figure out the s,t coordinates of the sprite we are copying out of */
    int twidth = sh - sl + 1;
    int theight = th - tl + 1;

    /* Figure out the power of two this sprite fits into */
    uint32_t real_width  = __rdp_round_to_power( twidth );
    uint32_t real_height = __rdp_round_to_power( theight );
    uint32_t wbits = __rdp_log2( real_width );
    uint32_t hbits = __rdp_log2( real_height );

    /* Because we are dividing by 8, we want to round up if we have a remainder */
    int16_t round_amount = (real_width % 8) ? 1 : 0;

    /* Instruct the RDP to copy the sprite data out */
    __rdp_ringbuffer_queue( 0xF5000000 | ((sprite->bitdepth == 2) ? 0x00100000 : 0x00180000) | 
                                       (((((real_width / 8) + round_amount) * sprite->bitdepth) & 0x1FF) << 9));
    __rdp_ringbuffer_queue( (mirror_enabled == 1 ? 0x40100 : 0) | (hbits << 14 ) | (wbits << 4) );
    __rdp_ringbuffer_send();

    /* Copying out only a chunk this time */
    __rdp_ringbuffer_queue( 0xF4000000 | (((sl << 2) & 0xFFF) << 12) | ((tl << 2) & 0xFFF) );
    __rdp_ringbuffer_queue( (((sh << 2) & 0xFFF) << 12) | ((th << 2) & 0xFFF) );
    __rdp_ringbuffer_send();

    /* Save sprite width and height for managed sprite commands */
    cache.width = twidth - 1;
    cache.height = theight - 1;
    cache.s = sl;
    cache.t = tl;
    
    /* Return the amount of texture memory consumed by this texture */
    return ((real_width / 8) + round_amount) * 8 * real_height * sprite->bitdepth;
}
This second one is used for drawing the texture once is loaded into tmem, so either when loading could be mirrored or now, i have added if ( tx < -cache.width ) { return; }, prevents system to hang when a sprite is out of screen from the left (it seems to be the only exception).

Code: Select all

void rdp_draw_textured_rectangle_scaled( int tx, int ty, int bx, int by, double x_scale, double y_scale )
{
    uint16_t s = cache.s << 5;
    uint16_t t = cache.t << 5;

    /* Cant display < 0, so must clip size and move S,T coord accordingly */
    if( tx < 0 )
    {
	     if ( tx < -cache.width ) { return; }
        s += (int)(((double)((-tx) << 5)) * (1.0 / x_scale));
        tx = 0;
    }

    if( ty < 0 )
    {
        t += (int)(((double)((-ty) << 5)) * (1.0 / y_scale));
        ty = 0;
    }

    /* Calculate the scaling constants based on a 6.10 fixed point system */
    int xs = (int)((1.0 / x_scale) * 4096.0);
    int ys = (int)((1.0 / y_scale) * 1024.0);

    /* Set up rectangle position in screen space */
    __rdp_ringbuffer_queue( 0xE4000000 | (bx << 14) | (by << 2) );
    __rdp_ringbuffer_queue( (tx << 14) | (ty << 2) );

    /* Set up texture position and scaling to 1:1 copy */
    __rdp_ringbuffer_queue( (s << 16) | t );
    __rdp_ringbuffer_queue( (xs & 0xFFFF) << 16 | (ys & 0xFFFF) );

    /* Send command */
    __rdp_ringbuffer_send();
}
Attachments
get_pixel.rar
(62.14 KiB) Downloaded 619 times
test3.rar
(42.22 KiB) Downloaded 636 times
test2.rar
(87.83 KiB) Downloaded 632 times
test1.rar
(41.91 KiB) Downloaded 613 times
Last edited by BMBx64 on Thu May 25, 2017 2:14 pm, edited 2 times in total.
User avatar
getafixx
Posts: 373
Joined: Tue Dec 04, 2012 3:28 pm
Location: Canada

Re: N64 programming (libdragon)

Post by getafixx »

Awesome stuff!

The SOTN demo worked really well, music played until it got to BGM 12, then it stalled the system. Had to reset.

Tried again, skipping tracks to get the BGM 12 which now worked, but when I skipped to BGM 13 it stalled again.

This is testing on an Everdrive64.
User avatar
Drew Sebastino
Formerly Espozo
Posts: 3496
Joined: Mon Sep 15, 2014 4:35 pm
Location: Richmond, Virginia

Re: N64 programming (libdragon)

Post by Drew Sebastino »

BMBx64 wrote:i don't know a place for N64 scene
Because there isn't one. :?

May I ask, what is libdragon?

Also, I don't understand how the second explosion demo has such worse performance than the first. Are you drawing all the pieces of an explosion with the same graphic before swapping out the texture in the texture cache? This should improve GPU performance, put then you'd also have to use the hardware z-buffer when drawing the explosion pieces instead of just sorting the order they are drawn with the CPU, which may negate the performance increase from changing the texture cache less often.
tepples
Posts: 22705
Joined: Sun Sep 19, 2004 11:12 pm
Location: NE Indiana, USA (NTSC)
Contact:

Re: N64 programming (libdragon)

Post by tepples »

A Google search for n64 libdragon brings up a library for N64 homebrew development by the developer of DSOrganize.
User avatar
Drew Sebastino
Formerly Espozo
Posts: 3496
Joined: Mon Sep 15, 2014 4:35 pm
Location: Richmond, Virginia

Re: N64 programming (libdragon)

Post by Drew Sebastino »

I actually did Google libdragon, but I didn't really know what I was looking at. Is it a full game engine, or routines that could be used to make one? Seems pretty limited if it apparently can't support 4bpp or 8bpp textures, which shouldn't have been any harder to implement than 16bpp or 24bpp or 32bpp textures other than a palette also needs to be uploaded. (Also to the texture cache?)
adam_smasher
Posts: 271
Joined: Sun Mar 27, 2011 10:49 am
Location: Victoria, BC

Re: N64 programming (libdragon)

Post by adam_smasher »

Espozo wrote:...but then you'd also have to use the hardware z-buffer when drawing the explosion pieces instead of just sorting the order they are drawn with the CPU, which may negate the performance increase from changing the texture cache less often.
I don't know the first thing about N64 development, but isn't the whole point of a hardware Z-buffer that it's faster than sorting polys on the CPU?
tepples
Posts: 22705
Joined: Sun Sep 19, 2004 11:12 pm
Location: NE Indiana, USA (NTSC)
Contact:

Re: N64 programming (libdragon)

Post by tepples »

I imagine that libdragon is analogous to Shiru's neslib.

Using a depth buffer incurs an extra memory read per pixel and write per drawn pixel, and it doesn't work for translucent objects. I don't know enough about the Nintendo 64's memory controller to know under what exact conditions the tradeoff is favorable. It was common on the PlayStation to use a bucket sort based on the Z coordinate of the centroid, which is fairly fast provided there are no pathological cases of large triangles close together.
User avatar
Drew Sebastino
Formerly Espozo
Posts: 3496
Joined: Mon Sep 15, 2014 4:35 pm
Location: Richmond, Virginia

Re: N64 programming (libdragon)

Post by Drew Sebastino »

adam_smasher wrote:isn't the whole point of a hardware Z-buffer that it's faster than sorting polys on the CPU?
Yes. However, using the hardware Z-buffer is slower for the GPU. If you are not changing out the texture cache for identical pieces in this case, you have to use the hardware Z-buffer, because you won't be able to sort the pieces of the explosions on a per pixel level by the CPU like the hardware Z-buffer will. You can avoid using the hardware Z-buffer by writing entire explosions in order, but then you have to change out the texture cache for every piece being drawn.
adam_smasher
Posts: 271
Joined: Sun Mar 27, 2011 10:49 am
Location: Victoria, BC

Re: N64 programming (libdragon)

Post by adam_smasher »

Yeah, I get it - I guess I'm just surprised using the Z-buffer is so much slower that it might be better to do CPU sorting & constantly trash the texture cache when you're not even pushing the GPU that hard (5000 unshaded unlit triangles per second). And that's not an "I'm surprised because that sounds unlikely and think you're wrong" kind of surprised, but a genuine, "woah, the N64's Z-buffer must be terrible" kind of surprised.
BMBx64
Posts: 25
Joined: Thu May 25, 2017 7:27 am

Re: N64 programming (libdragon)

Post by BMBx64 »

getafixx wrote:Awesome stuff!

The SOTN demo worked really well, music played until it got to BGM 12, then it stalled the system. Had to reset.

Tried again, skipping tracks to get the BGM 12 which now worked, but when I skipped to BGM 13 it stalled again.

This is testing on an Everdrive64.
Thanks :D , not sure why it crashes, do you use expansion pak? I have PAL N64 with expansion and works fine forcing PAL or NTSC modes on Everdrive 2.5.

However i know fopen/fclose have a memory leak, once the 100th file is open the next ones will fail (no handles), this can be "fixed" by using (dfs_open and dfs_close variant), i have to look at it, audio specially has been hard on N64 so far.

Here a bit more info about this memory leak:
https://krikzz.com/forum/index.php?topic=5969.15

Espozo wrote:Also, I don't understand how the second explosion demo has such worse performance than the first. Are you drawing all the pieces of an explosion with the same graphic before swapping out the texture in the texture cache? This should improve GPU performance, put then you'd also have to use the hardware z-buffer when drawing the explosion pieces instead of just sorting the order they are drawn with the CPU, which may negate the performance increase from changing the texture cache less often.
I wanted to check how goes the performance when textures are loaded without strategy, just to imagine worst scenarios, the sorting is done by the order in the program, a loop from 0 to max number of explosions, i don't know if Z-buffer is available for 2D rectangles.

--

For 8bit palettes, the low 2KB of the TMEM are filled with data, but the higher 2KB needs the color table lookup, then there are different formats, the grayscale ones works different i think, you can take more TMEM and set the color on the vertex.

I may be interested specially on 4bit palette support (MD,SNES,etc), but i hope to fix all the basic things, like flip a sprite if anyone can help a bit with the rdp.c code i can do all kind of tests.

--

I forgot this one..

TEST: LAYERS
Small tiled scroll that manages unlimited layers (they impact performance at some point), some features:
- Layers can have a custom number of rows and columns
- Different tile sizes in each layer separately (ex: main scroll layer 32x32, background layer 64x32)
- Layers can be set to repeat horizontally or vertically.

This pic shows 8 full screen layers (320x240x16), each one with unique texture:
Image

CONTROLS
Push joystick to move camera at different speed

DOWNLOAD
https://mega.nz/#!IhgTwJwK!AK67vHgNl6LpGoasvQtTuD1wGbYBGynOKYNT0yek6PE
Attachments
test4.rar
(40.2 KiB) Downloaded 621 times
Last edited by BMBx64 on Thu May 25, 2017 2:11 pm, edited 1 time in total.
lidnariq
Posts: 11429
Joined: Sun Apr 13, 2008 11:12 am

Re: N64 programming (libdragon)

Post by lidnariq »

BMBx64, you should feel free to use the attachment interface here for anything that doesn't infringe copyright.

Especially when they're only 40 KiB
User avatar
Drew Sebastino
Formerly Espozo
Posts: 3496
Joined: Mon Sep 15, 2014 4:35 pm
Location: Richmond, Virginia

Re: N64 programming (libdragon)

Post by Drew Sebastino »

You know, I was going to post some thoughts I had on these demos and ideas on how to improve them, but I realized I really have no clue how exactly the N64 really works. I think the GPU is split into two parts, the RSP and the RDP, with the RSP being programmable and running the non-programmable RDP. Each one has 4KB of ram, with the RSP's ram holding instruction data and the RDP's ram holding texture data. What I really want to know, is how is the data changed? Is it by DMA initiated by the CPU, because if so, it seems the CPU would be doing nothing but waiting for the GPU to be done with its current task (like writing to the SPC700 on the SNES), unless the GPU can actually generate an interrupt.
tepples
Posts: 22705
Joined: Sun Sep 19, 2004 11:12 pm
Location: NE Indiana, USA (NTSC)
Contact:

Re: N64 programming (libdragon)

Post by tepples »

As I understand it: Ideally, while the RSP is rendering a frame, the CPU is running game logic and generating the display list for the RSP to render in the next frame.
BMBx64
Posts: 25
Joined: Thu May 25, 2017 7:27 am

Re: N64 programming (libdragon)

Post by BMBx64 »

I finally added hardware flip feature, i will bring an example later.

Instead of reverse texture on the load i though it was more wise when drawing, there could be plenty of uses without reload such as axial symmetry.

I had to debug first how it works, the textures seems to be power of two sizes, for example mario texture is 16x31 and its rounded to 16x32, s and t coords have to be set accordingly to avoid garbage.
Image

The performance seems to be about the same when setting MIRROR_ENABLED on the RDP (for hardware flip) so i removed some flags from rdp_load, this function previously used to be:
rdp_load_texture (uint32_t texslot, uint32_t texloc, mirror_t mirror_enabled, sprite_t *sprite)

Now its just the texture call (which is faster):
rdp_load_texture ( sprite_t *sprite )

Flags for the flip:
0 - Normal
1 - Horizontally
2 - Vertically
3 - Both

rdp.c

Changes on rdp_load:

Code: Select all

static uint32_t __rdp_load_texture(sprite_t *sprite, int sl, int tl, int sh, int th )
{
    /* Invalidate data associated with sprite in cache */
    if( flush_strategy == FLUSH_STRATEGY_AUTOMATIC )
    {
        data_cache_hit_writeback_invalidate( sprite->data, sprite->width * sprite->height * sprite->bitdepth );
    }

    /* Point the RDP at the actual sprite data */
    __rdp_ringbuffer_queue( 0xFD000000 | ((sprite->bitdepth == 2) ? 0x00100000 : 0x00180000) | (sprite->width - 1) );
    __rdp_ringbuffer_queue( (uint32_t)sprite->data );
    __rdp_ringbuffer_send();

    /* Figure out the s,t coordinates of the sprite we are copying out of */
    int twidth = sh - sl + 1;
    int theight = th - tl + 1;
	 uint8_t mirror_enabled = 0;

    /* Figure out the power of two this sprite fits into */
    uint32_t real_width  = __rdp_round_to_power( twidth );
    uint32_t real_height = __rdp_round_to_power( theight );
    uint32_t wbits = __rdp_log2( real_width );
    uint32_t hbits = __rdp_log2( real_height );

    /* Because we are dividing by 8, we want to round up if we have a remainder */
    int16_t round_amount = (real_width % 8) ? 1 : 0;

    /* Instruct the RDP to copy the sprite data out */
    __rdp_ringbuffer_queue( 0xF5000000 | ((sprite->bitdepth == 2) ? 0x00100000 : 0x00180000) | 
                                       (((((real_width / 8) + round_amount) * sprite->bitdepth) & 0x1FF) << 9));
    __rdp_ringbuffer_queue( (mirror_enabled == 0 ? 0x40100 : 0) | (hbits << 14 ) | (wbits << 4) );
    __rdp_ringbuffer_send();

    /* Copying out only a chunk this time */
    __rdp_ringbuffer_queue( 0xF4000000 | (((sl << 2) & 0xFFF) << 12) | ((tl << 2) & 0xFFF) );
    __rdp_ringbuffer_queue( (((sh << 2) & 0xFFF) << 12) | ((th << 2) & 0xFFF) );
    __rdp_ringbuffer_send();

    /* Save sprite width and height for managed sprite commands */
    cache.width = twidth - 1;
    cache.height = theight - 1;
    cache.s = sl;
    cache.t = tl;
	 cache.real_width = real_width;
	 cache.real_height = real_height;
    
    /* Return the amount of texture memory consumed by this texture */
    return ((real_width / 8) + round_amount) * 8 * real_height * sprite->bitdepth;
}
Changes on rdp_draw_textured_rectangle_scaled, still i have to test different texture sizes just to make sure its correct.

Code: Select all

	if (flags > 0)
	{	
		if (flags != 2)
			s += ( (cache.width+1) + ((cache.real_width-(cache.width+1))<<1) ) << 5;
	
		if (flags != 1)
			t += ( (cache.height+1) + ((cache.real_height-(cache.height+1))<<1) ) << 5;	
	}

Code: Select all

void __rdp_draw_textured_rectangle_scaled( int tx, int ty, int bx, int by, double x_scale, double y_scale, int flags )
{
    uint16_t s = cache.s << 5;
    uint16_t t = cache.t << 5;

    /* Cant display < 0, so must clip size and move S,T coord accordingly */
    if( tx < 0 )
    {
		if ( tx < -cache.width ) { return; }
        s += (int)(((double)((-tx) << 5)) * (1.0 / x_scale));
        tx = 0;
    }

    if( ty < 0 )
    {
        t += (int)(((double)((-ty) << 5)) * (1.0 / y_scale));
        ty = 0;
    }

    /* Calculate the scaling constants based on a 6.10 fixed point system */
    int xs = (int)((1.0 / x_scale) * 4096.0);
    int ys = (int)((1.0 / y_scale) * 1024.0);

	 if (flags > 0)
	 {	
		 if (flags != 2)
			 s += ( (cache.width+1) + ((cache.real_width-(cache.width+1))<<1) ) << 5;
	
		 if (flags != 1)
			 t += ( (cache.height+1) + ((cache.real_height-(cache.height+1))<<1) ) << 5;	
	 }
	
    /* Set up rectangle position in screen space */
    __rdp_ringbuffer_queue( 0xE4000000 | (bx << 14) | (by << 2) );
    __rdp_ringbuffer_queue( (tx << 14) | (ty << 2) );

    /* Set up texture position and scaling to 1:1 copy */
    __rdp_ringbuffer_queue( (s << 16) | t );
    __rdp_ringbuffer_queue( (xs & 0xFFFF) << 16 | (ys & 0xFFFF) );

    /* Send command */
    __rdp_ringbuffer_send();
}
I tried it combined with scale_x and scale_y, seems to be right with scale_y (also mid screen, with clipping):
Image

However i tested it with scale_x and pain arrived, i tried it without my code and still does the same, maybe a bug on the lib? I have to look at it.
Image
lidnariq
Posts: 11429
Joined: Sun Apr 13, 2008 11:12 am

Re: N64 programming (libdragon)

Post by lidnariq »

BMBx64 wrote:However i tested it with scale_x and pain arrived, i tried it without my code and still does the same, maybe a bug on the lib? I have to look at it.
Image
Looks like it's reading the source pixels four pixels as a time...
Post Reply