2014-12-29 21:05:35 +00:00
/*
Basic gdrom syscall emulation
Adapted from some ( very ) old pre - nulldc hle code
2019-07-30 17:04:51 +00:00
Bits and pieces from redream ( https : //github.com/inolen/redream)
2014-12-29 21:05:35 +00:00
*/
2019-09-07 12:37:39 +00:00
# include <cstdio>
2014-12-29 21:05:35 +00:00
# include "types.h"
# include "hw/sh4/sh4_mem.h"
2019-08-08 06:24:13 +00:00
# include "hw/sh4/sh4_sched.h"
# include "hw/sh4/sh4_core.h"
# undef r
2014-12-29 21:05:35 +00:00
# include "gdrom_hle.h"
2019-07-30 17:04:51 +00:00
# include "hw/gdrom/gdromv3.h"
2019-08-03 17:20:30 +00:00
# include "hw/holly/holly_intc.h"
2019-07-30 17:04:51 +00:00
# include "reios.h"
2020-01-30 17:59:26 +00:00
# include "imgread/common.h"
2014-12-29 21:05:35 +00:00
2019-09-07 12:37:39 +00:00
# include <algorithm>
2015-02-25 18:33:36 +00:00
# define SWAP32(a) ((((a) & 0xff) << 24) | (((a) & 0xff00) << 8) | (((a) >> 8) & 0xff00) | (((a) >> 24) & 0xff))
2019-07-01 16:23:10 +00:00
# define debugf(...) DEBUG_LOG(REIOS, __VA_ARGS__)
2014-12-29 21:05:35 +00:00
2019-08-08 06:24:13 +00:00
gdrom_hle_state_t gd_hle_state = { 0xffffffff , 2 , BIOS_INACTIVE } ;
2019-08-01 10:31:08 +00:00
static void GDROM_HLE_ReadSES ( )
2014-12-29 21:05:35 +00:00
{
2019-08-01 10:31:08 +00:00
u32 s = gd_hle_state . params [ 0 ] ;
u32 b = gd_hle_state . params [ 1 ] ;
u32 ba = gd_hle_state . params [ 2 ] ;
u32 bb = gd_hle_state . params [ 3 ] ;
2014-12-29 21:05:35 +00:00
2019-08-31 19:56:47 +00:00
INFO_LOG ( REIOS , " GDROM_HLE_ReadSES: doing nothing w/ %d, %d, %d, %d " , s , b , ba , bb ) ;
2014-12-29 21:05:35 +00:00
}
2019-08-01 10:31:08 +00:00
static void GDROM_HLE_ReadTOC ( )
2014-12-29 21:05:35 +00:00
{
2019-08-01 10:31:08 +00:00
u32 area = gd_hle_state . params [ 0 ] ;
u32 dest = gd_hle_state . params [ 1 ] ;
2014-12-29 21:05:35 +00:00
2019-07-30 17:04:51 +00:00
debugf ( " GDROM READ TOC : %X %X " , area , dest ) ;
if ( area = = DoubleDensity & & libGDR_GetDiscType ( ) ! = GdRom )
{
// Only GD-ROM has a high-density area but no error is reported
2019-08-01 10:31:08 +00:00
gd_hle_state . status = BIOS_INACTIVE ;
2019-07-30 17:04:51 +00:00
return ;
}
2015-02-25 18:33:36 +00:00
2019-08-03 17:20:30 +00:00
u32 toc [ 102 ] ;
libGDR_GetToc ( toc , area ) ;
2014-12-29 21:05:35 +00:00
2019-07-30 17:04:51 +00:00
// Swap results to LE
2015-02-25 18:33:36 +00:00
for ( int i = 0 ; i < 102 ; i + + ) {
2019-08-03 17:20:30 +00:00
toc [ i ] = SWAP32 ( toc [ i ] ) ;
}
if ( ! mmu_enabled ( ) )
{
u32 * pDst = ( u32 * ) GetMemPtr ( dest , sizeof ( toc ) ) ;
if ( pDst ! = NULL )
{
memcpy ( pDst , toc , sizeof ( toc ) ) ;
return ;
}
2015-02-25 18:33:36 +00:00
}
2019-08-03 17:20:30 +00:00
for ( int i = 0 ; i < 102 ; i + + , dest + = 4 )
WriteMem32 ( dest , toc [ i ] ) ;
2014-12-29 21:05:35 +00:00
}
2019-08-03 17:20:30 +00:00
template < bool virtual_addr >
static void read_sectors_to ( u32 addr , u32 sector , u32 count )
{
2019-08-08 06:24:13 +00:00
gd_hle_state . cur_sector = sector + count - 1 ;
if ( virtual_addr )
gd_hle_state . xfer_end_time = 0 ;
2021-07-06 14:19:12 +00:00
else if ( count > 5 & & ! config : : FastGDRomLoad )
2019-08-08 06:24:13 +00:00
// Large Transfers: GD-ROM rate (approx. 1.8 MB/s)
gd_hle_state . xfer_end_time = sh4_sched_now64 ( ) + ( u64 ) count * 2048 * 1000000L / 10240 ;
else
// Small transfers: Max G1 bus rate: 50 MHz x 16 bits
gd_hle_state . xfer_end_time = sh4_sched_now64 ( ) + 5 * 2048 * 2 ;
2019-08-03 17:20:30 +00:00
if ( ! virtual_addr | | ! mmu_enabled ( ) )
{
u8 * pDst = GetMemPtr ( addr , 0 ) ;
2014-12-30 00:57:13 +00:00
2019-08-03 17:20:30 +00:00
if ( pDst ! = NULL )
{
libGDR_ReadSector ( pDst , sector , count , 2048 ) ;
return ;
}
2014-12-30 00:57:13 +00:00
}
2019-08-03 17:20:30 +00:00
u32 temp [ 2048 / 4 ] ;
2014-12-30 00:57:13 +00:00
2019-08-03 17:20:30 +00:00
while ( count > 0 )
{
libGDR_ReadSector ( ( u8 * ) temp , sector , 1 , sizeof ( temp ) ) ;
2014-12-30 00:57:13 +00:00
2019-09-07 12:37:39 +00:00
for ( std : : size_t i = 0 ; i < ARRAY_SIZE ( temp ) ; i + + )
2019-08-03 17:20:30 +00:00
{
if ( virtual_addr )
2014-12-30 00:57:13 +00:00
WriteMem32 ( addr , temp [ i ] ) ;
2019-08-03 17:20:30 +00:00
else
WriteMem32_nommu ( addr , temp [ i ] ) ;
addr + = 4 ;
2014-12-30 00:57:13 +00:00
}
2019-08-03 17:20:30 +00:00
sector + + ;
count - - ;
2014-12-30 00:57:13 +00:00
}
}
2019-08-01 10:31:08 +00:00
static void GDROM_HLE_ReadDMA ( )
2014-12-29 21:05:35 +00:00
{
2019-08-01 10:31:08 +00:00
u32 s = gd_hle_state . params [ 0 ] ;
u32 n = gd_hle_state . params [ 1 ] ;
u32 b = gd_hle_state . params [ 2 ] ;
u32 u = gd_hle_state . params [ 3 ] ;
2014-12-29 21:05:35 +00:00
2019-07-30 17:04:51 +00:00
debugf ( " GDROM: DMA READ Sector=%d, Num=%d, Buffer=0x%08X, Unk01=0x%08X " , s , n , b , u ) ;
2019-08-03 17:20:30 +00:00
read_sectors_to < false > ( b , s , n ) ;
2019-08-08 06:24:13 +00:00
gd_hle_state . result [ 2 ] = 0 ;
gd_hle_state . result [ 3 ] = 0 ;
2014-12-29 21:05:35 +00:00
}
2019-08-01 10:31:08 +00:00
static void GDROM_HLE_ReadPIO ( )
2014-12-29 21:05:35 +00:00
{
2019-08-01 10:31:08 +00:00
u32 s = gd_hle_state . params [ 0 ] ;
u32 n = gd_hle_state . params [ 1 ] ;
u32 b = gd_hle_state . params [ 2 ] ;
u32 u = gd_hle_state . params [ 3 ] ;
2014-12-29 21:05:35 +00:00
2019-07-30 17:04:51 +00:00
debugf ( " GDROM: PIO READ Sector=%d, Num=%d, Buffer=0x%08X, Unk01=0x%08X " , s , n , b , u ) ;
2014-12-29 21:05:35 +00:00
2019-08-03 17:20:30 +00:00
read_sectors_to < true > ( b , s , n ) ;
2019-08-01 10:31:08 +00:00
gd_hle_state . result [ 2 ] = n * 2048 ;
2019-08-08 06:24:13 +00:00
gd_hle_state . result [ 3 ] = 0 ;
2014-12-29 21:05:35 +00:00
}
2019-08-01 10:31:08 +00:00
static void GDCC_HLE_GETSCD ( ) {
u32 format = gd_hle_state . params [ 0 ] ;
u32 size = gd_hle_state . params [ 1 ] ;
u32 dest = gd_hle_state . params [ 2 ] ;
2014-12-29 21:05:35 +00:00
2019-09-19 09:36:59 +00:00
DEBUG_LOG ( REIOS , " GDROM: GETSCD format %x size %x dest %08x " , format , size , dest ) ;
2014-12-29 21:05:35 +00:00
2020-01-30 17:59:26 +00:00
if ( libGDR_GetDiscType ( ) = = Open | | libGDR_GetDiscType ( ) = = NoDisk )
{
gd_hle_state . status = BIOS_ERROR ;
gd_hle_state . result [ 0 ] = 2 ; // ?
2020-01-31 17:33:16 +00:00
gd_hle_state . result [ 1 ] = 0 ;
gd_hle_state . result [ 2 ] = 0 ;
gd_hle_state . result [ 3 ] = 0 ;
2020-01-30 17:59:26 +00:00
return ;
}
if ( sns_asc ! = 0 )
{
// Helps D2 detect the disk change
gd_hle_state . status = BIOS_ERROR ;
gd_hle_state . result [ 0 ] = sns_key ;
gd_hle_state . result [ 1 ] = sns_asc ;
2020-01-31 17:33:16 +00:00
gd_hle_state . result [ 2 ] = 0x18 ; // ?
gd_hle_state . result [ 3 ] = sns_ascq ; // ?
2020-01-30 17:59:26 +00:00
sns_key = 0 ;
sns_asc = 0 ;
sns_ascq = 0 ;
return ;
}
2020-06-17 20:58:26 +00:00
if ( cdda . status = = cdda_t : : Playing )
2019-08-01 10:31:08 +00:00
gd_hle_state . cur_sector = cdda . CurrAddr . FAD ;
2019-07-30 17:04:51 +00:00
u8 scd [ 100 ] ;
2019-08-01 10:31:08 +00:00
gd_get_subcode ( format , gd_hle_state . cur_sector , scd ) ;
2019-07-30 17:04:51 +00:00
verify ( scd [ 3 ] = = size ) ;
2019-08-03 17:20:30 +00:00
if ( ! mmu_enabled ( ) & & GetMemPtr ( dest , size ) ! = NULL )
memcpy ( GetMemPtr ( dest , size ) , scd , size ) ;
else
{
2020-03-29 18:58:49 +00:00
for ( u32 i = 0 ; i < size ; i + + )
2019-08-03 17:20:30 +00:00
WriteMem8 ( dest + + , scd [ i ] ) ;
}
2019-07-30 17:04:51 +00:00
// record size of pio transfer to gdrom
2019-08-01 10:31:08 +00:00
gd_hle_state . result [ 2 ] = size ;
2019-07-30 17:04:51 +00:00
}
2019-08-03 17:20:30 +00:00
template < bool dma >
static void multi_xfer ( )
{
u32 dest = gd_hle_state . params [ 0 ] ;
u32 size = gd_hle_state . params [ 1 ] ;
2014-12-29 21:05:35 +00:00
2019-08-03 17:20:30 +00:00
size = std : : min ( size , gd_hle_state . multi_read_count ) ;
while ( size > 0 )
{
u8 buf [ 2048 ] ;
libGDR_ReadSector ( buf , gd_hle_state . multi_read_sector , 1 , 2048 ) ;
while ( size > 0 )
{
int remaining = 2048 - gd_hle_state . multi_read_offset ;
2019-08-31 18:53:42 +00:00
if ( size > = 4 & & remaining > = 4 & & ( dest & 3 ) = = 0 )
2019-08-03 17:20:30 +00:00
{
if ( dma )
WriteMem32_nommu ( dest , * ( u32 * ) & buf [ gd_hle_state . multi_read_offset ] ) ;
else
WriteMem32 ( dest , * ( u32 * ) & buf [ gd_hle_state . multi_read_offset ] ) ;
dest + = 4 ;
gd_hle_state . multi_read_offset + = 4 ;
gd_hle_state . multi_read_count - = 4 ;
size - = 4 ;
}
2019-08-31 18:53:42 +00:00
else if ( size > = 2 & & remaining > = 2 & & ( dest & 1 ) = = 0 )
2019-08-03 17:20:30 +00:00
{
if ( dma )
WriteMem16_nommu ( dest , * ( u16 * ) & buf [ gd_hle_state . multi_read_offset ] ) ;
else
WriteMem16 ( dest , * ( u16 * ) & buf [ gd_hle_state . multi_read_offset ] ) ;
dest + = 2 ;
gd_hle_state . multi_read_offset + = 2 ;
gd_hle_state . multi_read_count - = 2 ;
size - = 2 ;
}
else
{
if ( dma )
WriteMem8_nommu ( dest , buf [ gd_hle_state . multi_read_offset ] ) ;
else
WriteMem8 ( dest , buf [ gd_hle_state . multi_read_offset ] ) ;
dest + + ;
gd_hle_state . multi_read_offset + + ;
gd_hle_state . multi_read_count - - ;
size - - ;
}
if ( gd_hle_state . multi_read_offset > = 2048 )
{
verify ( gd_hle_state . multi_read_offset = = 2048 ) ;
gd_hle_state . multi_read_sector + + ;
gd_hle_state . multi_read_offset = 0 ;
break ;
}
}
}
2019-08-04 17:09:02 +00:00
if ( ! dma )
{
gd_hle_state . result [ 2 ] = gd_hle_state . multi_read_total - gd_hle_state . multi_read_count ;
2019-08-19 15:19:27 +00:00
gd_hle_state . result [ 3 ] = 0 ; // 1 hangs Bust-a-Move-4
2019-08-08 06:24:13 +00:00
if ( gd_hle_state . multi_callback ! = 0 )
{
Sh4cntx . r [ 4 ] = gd_hle_state . multi_callback_arg ;
Sh4cntx . pc = gd_hle_state . multi_callback ;
}
2019-08-04 17:09:02 +00:00
}
else
{
2019-08-04 17:46:46 +00:00
gd_hle_state . result [ 2 ] = 2048 ;
2019-08-04 17:09:02 +00:00
gd_hle_state . result [ 3 ] = gd_hle_state . multi_read_count > 0 ? 1 : 0 ;
gd_hle_state . dma_trans_ended = true ;
2019-08-08 06:24:13 +00:00
if ( gd_hle_state . multi_read_count = = 0 )
gd_hle_state . status = BIOS_COMPLETED ;
2019-08-03 17:20:30 +00:00
asic_RaiseInterrupt ( holly_GDROM_DMA ) ;
}
}
2014-12-29 21:05:35 +00:00
u32 SecMode [ 4 ] ;
2019-08-01 10:31:08 +00:00
static void GD_HLE_Command ( u32 cc )
2014-12-29 21:05:35 +00:00
{
switch ( cc )
{
case GDCC_GETTOC :
2019-08-01 10:31:08 +00:00
WARN_LOG ( REIOS , " GDROM: *FIXME* CMD GETTOC " ) ;
2014-12-29 21:05:35 +00:00
break ;
case GDCC_GETTOC2 :
2019-08-01 10:31:08 +00:00
GDROM_HLE_ReadTOC ( ) ;
2014-12-29 21:05:35 +00:00
break ;
case GDCC_GETSES :
2019-08-01 10:31:08 +00:00
GDROM_HLE_ReadSES ( ) ;
2014-12-29 21:05:35 +00:00
break ;
case GDCC_INIT :
2019-09-19 09:36:59 +00:00
DEBUG_LOG ( REIOS , " GDROM: CMD INIT " ) ;
2019-08-03 17:20:30 +00:00
gd_hle_state . multi_callback = 0 ;
gd_hle_state . multi_read_count = 0 ;
2020-06-17 20:58:26 +00:00
cdda . status = cdda_t : : NoInfo ;
2014-12-29 21:05:35 +00:00
break ;
case GDCC_PIOREAD :
2019-08-01 10:31:08 +00:00
GDROM_HLE_ReadPIO ( ) ;
2019-08-03 17:20:30 +00:00
SecNumber . Status = GD_STANDBY ;
2020-06-17 20:58:26 +00:00
cdda . status = cdda_t : : NoInfo ;
2014-12-29 21:05:35 +00:00
break ;
case GDCC_DMAREAD :
2020-06-17 20:58:26 +00:00
cdda . status = cdda_t : : NoInfo ;
2019-08-08 06:24:13 +00:00
if ( gd_hle_state . xfer_end_time = = 0 )
GDROM_HLE_ReadDMA ( ) ;
if ( gd_hle_state . xfer_end_time > 0 )
{
if ( gd_hle_state . xfer_end_time > sh4_sched_now64 ( ) )
return ;
gd_hle_state . xfer_end_time = 0 ;
}
gd_hle_state . result [ 2 ] = gd_hle_state . params [ 1 ] * 2048 ;
gd_hle_state . result [ 3 ] = 0 ;
2019-08-03 17:20:30 +00:00
SecNumber . Status = GD_STANDBY ;
2014-12-29 21:05:35 +00:00
break ;
case GDCC_PLAY_SECTOR :
2019-09-19 09:36:59 +00:00
{
u32 start_fad = gd_hle_state . params [ 0 ] ;
u32 end_fad = gd_hle_state . params [ 1 ] ;
DEBUG_LOG ( REIOS , " GDROM: CMD PLAYSEC from %d to %d repeats %d " , start_fad , end_fad , gd_hle_state . params [ 2 ] ) ;
2020-06-17 20:58:26 +00:00
cdda . status = cdda_t : : Playing ;
2019-09-19 09:36:59 +00:00
cdda . StartAddr . FAD = start_fad ;
cdda . EndAddr . FAD = end_fad ;
cdda . repeats = gd_hle_state . params [ 2 ] ;
cdda . CurrAddr . FAD = start_fad ;
SecNumber . Status = GD_PLAY ;
}
2014-12-29 21:05:35 +00:00
break ;
case GDCC_RELEASE :
2019-09-19 09:36:59 +00:00
DEBUG_LOG ( REIOS , " GDROM: CMD RELEASE " ) ;
2020-06-17 20:58:26 +00:00
if ( cdda . status = = cdda_t : : Paused )
cdda . status = cdda_t : : Playing ;
2019-09-19 09:36:59 +00:00
SecNumber . Status = GD_PLAY ;
2019-07-30 17:04:51 +00:00
break ;
case GDCC_STOP :
2019-09-19 09:36:59 +00:00
DEBUG_LOG ( REIOS , " GDROM: CMD STOP " ) ;
2020-06-17 20:58:26 +00:00
cdda . status = cdda_t : : NoInfo ;
2019-07-31 20:08:56 +00:00
SecNumber . Status = GD_STANDBY ;
2019-07-30 17:04:51 +00:00
break ;
case GDCC_SEEK :
2019-09-19 09:36:59 +00:00
DEBUG_LOG ( REIOS , " GDROM: CMD SEEK " ) ;
2020-06-17 20:58:26 +00:00
cdda . status = cdda_t : : Paused ;
2019-07-31 20:08:56 +00:00
SecNumber . Status = GD_PAUSE ;
2019-07-30 17:04:51 +00:00
break ;
case GDCC_PLAY :
2019-07-31 20:08:56 +00:00
{
2019-08-01 10:31:08 +00:00
u32 first_track = gd_hle_state . params [ 0 ] ;
u32 last_track = gd_hle_state . params [ 1 ] ;
u32 repeats = gd_hle_state . params [ 2 ] ;
2019-07-31 20:08:56 +00:00
u32 start_fad , end_fad , dummy ;
libGDR_GetTrack ( first_track , start_fad , dummy ) ;
libGDR_GetTrack ( last_track , dummy , end_fad ) ;
2019-09-19 09:36:59 +00:00
DEBUG_LOG ( REIOS , " GDROM: CMD PLAY first_track %x last_track %x repeats %x start_fad %x end_fad %x param4 %x " , first_track , last_track , repeats ,
2019-08-01 10:31:08 +00:00
start_fad , end_fad , gd_hle_state . params [ 3 ] ) ;
2020-06-17 20:58:26 +00:00
cdda . status = cdda_t : : Playing ;
2019-07-31 20:08:56 +00:00
cdda . StartAddr . FAD = start_fad ;
cdda . EndAddr . FAD = end_fad ;
cdda . repeats = repeats ;
if ( SecNumber . Status ! = GD_PAUSE | | cdda . CurrAddr . FAD < start_fad | | cdda . CurrAddr . FAD > end_fad )
cdda . CurrAddr . FAD = start_fad ;
SecNumber . Status = GD_PLAY ;
}
2014-12-29 21:05:35 +00:00
break ;
2019-07-30 17:04:51 +00:00
case GDCC_PAUSE :
2019-09-19 09:36:59 +00:00
DEBUG_LOG ( REIOS , " GDROM: CMD PAUSE " ) ;
2020-06-17 20:58:26 +00:00
if ( cdda . status = = cdda_t : : Playing )
cdda . status = cdda_t : : Paused ;
2019-07-31 20:08:56 +00:00
SecNumber . Status = GD_PAUSE ;
2019-07-30 17:04:51 +00:00
break ;
2014-12-29 21:05:35 +00:00
2019-08-04 17:09:02 +00:00
case GDCC_READ :
2019-07-30 17:04:51 +00:00
{
2019-08-04 17:09:02 +00:00
u32 sector = gd_hle_state . params [ 0 ] ;
u32 num = gd_hle_state . params [ 1 ] ;
2019-07-30 17:04:51 +00:00
2019-09-19 09:36:59 +00:00
DEBUG_LOG ( REIOS , " GDROM: CMD READ Sector=%d, Num=%d " , sector , num ) ;
2019-08-03 17:20:30 +00:00
gd_hle_state . status = BIOS_DATA_AVAIL ;
2019-08-04 17:09:02 +00:00
gd_hle_state . multi_read_sector = sector ;
gd_hle_state . multi_read_count = num * 2048 ;
2019-08-03 17:20:30 +00:00
gd_hle_state . multi_read_total = gd_hle_state . multi_read_count ;
gd_hle_state . multi_read_offset = 0 ;
2019-08-04 17:46:46 +00:00
gd_hle_state . result [ 2 ] = 2048 ;
2019-08-04 17:09:02 +00:00
gd_hle_state . result [ 3 ] = num > 0 ? 1 : 0 ;
2019-07-30 17:04:51 +00:00
}
2014-12-29 21:05:35 +00:00
break ;
case GDCC_GETSCD :
2019-08-01 10:31:08 +00:00
GDCC_HLE_GETSCD ( ) ;
2019-07-30 17:04:51 +00:00
break ;
case GDCC_REQ_MODE :
{
2019-08-01 10:31:08 +00:00
u32 dest = gd_hle_state . params [ 0 ] ;
debugf ( " GDROM: REQ_MODE dest:%x " , dest ) ;
2019-08-03 17:20:30 +00:00
WriteMem32 ( dest , GD_HardwareInfo . speed ) ;
WriteMem32 ( dest + 4 , ( GD_HardwareInfo . standby_hi < < 8 ) | GD_HardwareInfo . standby_lo ) ;
WriteMem32 ( dest + 8 , GD_HardwareInfo . read_flags ) ;
WriteMem32 ( dest + 12 , GD_HardwareInfo . read_retry ) ;
2019-07-30 17:04:51 +00:00
// record size of pio transfer to gdrom
2019-08-01 10:31:08 +00:00
gd_hle_state . result [ 2 ] = 0xa ;
2019-07-30 17:04:51 +00:00
}
break ;
case GDCC_SET_MODE :
{
2019-08-01 10:31:08 +00:00
u32 speed = gd_hle_state . params [ 0 ] ;
u32 standby = gd_hle_state . params [ 1 ] ;
u32 read_flags = gd_hle_state . params [ 2 ] ;
u32 read_retry = gd_hle_state . params [ 3 ] ;
2019-07-30 17:04:51 +00:00
2019-08-01 10:31:08 +00:00
debugf ( " GDROM: SET_MODE speed %x standby %x read_flags %x read_retry %x " , speed , standby , read_flags , read_retry ) ;
2019-07-30 17:04:51 +00:00
GD_HardwareInfo . speed = speed ;
GD_HardwareInfo . standby_hi = ( standby & 0xff00 ) > > 8 ;
GD_HardwareInfo . standby_lo = standby & 0xff ;
GD_HardwareInfo . read_flags = read_flags ;
GD_HardwareInfo . read_retry = read_retry ;
// record size of pio transfer to gdrom
2019-08-01 10:31:08 +00:00
gd_hle_state . result [ 2 ] = 0xa ;
2019-07-30 17:04:51 +00:00
}
break ;
case GDCC_GET_VER :
{
2019-08-01 10:31:08 +00:00
u32 dest = gd_hle_state . params [ 0 ] ;
2019-07-30 17:04:51 +00:00
debugf ( " GDROM: GDCC_GET_VER dest %x " , dest ) ;
char ver [ ] = " GDC Version 1.10 1999-03-31 " ;
u32 len = ( u32 ) strlen ( ver ) ;
// 0x8c0013b8 (offset 0xd0 in the gdrom state struct) is then loaded and
// overwrites the last byte. no idea what this is, but seems to be hard
// coded to 0x02 on boot
ver [ len - 1 ] = 0x02 ;
2019-09-07 12:37:39 +00:00
for ( u32 i = 0 ; i < len ; i + + )
2019-08-03 17:20:30 +00:00
WriteMem8 ( dest + + , ver [ i ] ) ;
2019-07-30 17:04:51 +00:00
}
2014-12-29 21:05:35 +00:00
break ;
2019-07-30 17:04:51 +00:00
case GDCC_REQ_STAT :
{
// odd, but this function seems to get passed 4 unique pointers
2019-08-01 10:31:08 +00:00
u32 dst0 = gd_hle_state . params [ 0 ] ;
u32 dst1 = gd_hle_state . params [ 1 ] ;
u32 dst2 = gd_hle_state . params [ 2 ] ;
u32 dst3 = gd_hle_state . params [ 3 ] ;
2019-07-30 17:04:51 +00:00
debugf ( " GDROM: GDCC_REQ_STAT dst0=%08x dst1=%08x dst2=%08x dst3=%08x " , dst0 , dst1 , dst2 , dst3 ) ;
// bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0
// byte | | | | | | | |
// ------------------------------------------------------
// 0 | 0 | 0 | 0 | 0 | status
// ------------------------------------------------------
// 1 | 0 | 0 | 0 | 0 | repeat count
// ------------------------------------------------------
// 2-3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0
2019-08-04 17:09:02 +00:00
// WriteMem32(dst0, (cdda.repeats << 8) | SecNumber.Status);
WriteMem32 ( dst0 , ( cdda . repeats < < 8 ) | ( SecNumber . Status = = GD_STANDBY ? GD_PAUSE : SecNumber . Status ) ) ;
2019-07-30 17:04:51 +00:00
// bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0
// byte | | | | | | | |
// ------------------------------------------------------
// 0 | subcode q track number
// ------------------------------------------------------
// 1-3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0
u32 elapsed ;
2019-08-01 10:31:08 +00:00
u32 tracknum = libGDR_GetTrackNumber ( gd_hle_state . cur_sector , elapsed ) ;
2019-07-30 17:04:51 +00:00
WriteMem32 ( dst1 , tracknum ) ;
// bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0
// byte | | | | | | | |
// ------------------------------------------------------
// 0-2 | fad (little-endian)
// ------------------------------------------------------
// 3 | address | control
2020-06-17 20:58:26 +00:00
u32 out = ( ( ( SecNumber . DiscFormat = = 0 ? 0 : 0x40 ) | 1 ) < < 24 )
| ( gd_hle_state . cur_sector & 0x00ffffff ) ;
2019-07-30 17:04:51 +00:00
WriteMem32 ( dst2 , out ) ;
// bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0
// byte | | | | | | | |
// ------------------------------------------------------
// 0 | subcode q index number
// ------------------------------------------------------
// 1-3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0
WriteMem32 ( dst3 , 1 ) ;
// record pio transfer size
2019-08-01 10:31:08 +00:00
gd_hle_state . result [ 2 ] = 0xa ;
2019-07-30 17:04:51 +00:00
}
break ;
2019-08-03 17:20:30 +00:00
case GDCC_MULTI_DMAREAD :
case GDCC_MULTI_PIOREAD :
{
u32 sector = gd_hle_state . params [ 0 ] ;
u32 num = gd_hle_state . params [ 1 ] ;
bool dma = cc = = GDCC_MULTI_DMAREAD ;
2019-09-19 09:36:59 +00:00
DEBUG_LOG ( REIOS , " GDROM: MULTI_%sREAD Sector=%d, Num=%d " , dma ? " DMA " : " PIO " , sector , num ) ;
2019-08-03 17:20:30 +00:00
gd_hle_state . status = BIOS_DATA_AVAIL ;
gd_hle_state . multi_read_sector = sector ;
gd_hle_state . multi_read_count = num * 2048 ;
gd_hle_state . multi_read_total = gd_hle_state . multi_read_count ;
gd_hle_state . multi_read_offset = 0 ;
// wild guesses here
gd_hle_state . result [ 2 ] = 0 ;
2019-08-08 06:24:13 +00:00
gd_hle_state . result [ 3 ] = 0 ;
2019-08-03 17:20:30 +00:00
}
break ;
case GDCC_REQ_DMA_TRANS :
case GDCC_REQ_PIO_TRANS :
{
u32 dest = gd_hle_state . params [ 0 ] ;
u32 size = gd_hle_state . params [ 1 ] ;
bool dma = cc = = GDCC_REQ_DMA_TRANS ;
2019-09-19 09:36:59 +00:00
DEBUG_LOG ( REIOS , " GDROM: REQ_%s_TRANS dest %x size %x " , dma ? " DMA " : " PIO " ,
2019-08-03 17:20:30 +00:00
dest , size ) ;
if ( dma )
multi_xfer < true > ( ) ;
else
multi_xfer < false > ( ) ;
}
break ;
2019-07-30 17:04:51 +00:00
default :
2019-08-01 10:31:08 +00:00
WARN_LOG ( REIOS , " GDROM: Unknown GDROM CC:%X " , cc ) ;
2019-07-30 17:04:51 +00:00
break ;
2014-12-29 21:05:35 +00:00
}
2019-08-03 17:20:30 +00:00
if ( gd_hle_state . status = = BIOS_ACTIVE )
gd_hle_state . status = BIOS_COMPLETED ;
2019-08-08 06:24:13 +00:00
gd_hle_state . command = - 1 ;
2014-12-29 21:05:35 +00:00
}
2019-07-30 17:04:51 +00:00
# define r Sh4cntx.r
2014-12-29 21:05:35 +00:00
void gdrom_hle_op ( )
{
if ( SYSCALL_GDROM = = r [ 6 ] ) // GDROM SYSCALL
{
switch ( r [ 7 ] ) // COMMAND CODE
{
2019-08-01 10:31:08 +00:00
case GDROM_SEND_COMMAND :
// Enqueue a command for the GDROM subsystem to execute.
//
// Args:
// r4 = command code
// r5 = pointer to parameter block for the command, can be NULL if the command does not take parameters
//
// Returns: a request id (>=0) if successful, negative error code if failed
2019-08-03 17:20:30 +00:00
debugf ( " GDROM: HLE SEND COMMAND CC:%X param ptr: %X bios status %d " , r [ 4 ] , r [ 5 ] , gd_hle_state . status ) ;
2019-08-01 10:31:08 +00:00
if ( gd_hle_state . status ! = BIOS_INACTIVE )
{
r [ 0 ] = 0 ;
}
else
{
2019-08-03 17:20:30 +00:00
for ( int i = 0 ; i < 4 ; i + + )
2019-08-08 06:24:13 +00:00
{
try {
gd_hle_state . params [ i ] = r [ 5 ] = = 0 ? 0 : ReadMem32 ( r [ 5 ] + i * 4 ) ;
} catch ( SH4ThrownException & ex ) {
// Ignore page faults. happens for commands not taking params
gd_hle_state . params [ i ] = 0 ;
}
}
2019-08-01 10:31:08 +00:00
memset ( gd_hle_state . result , 0 , sizeof ( gd_hle_state . result ) ) ;
2020-06-17 20:58:26 +00:00
if ( gd_hle_state . next_request_id = = ~ 0u | | gd_hle_state . next_request_id = = 0 )
2019-08-04 17:09:02 +00:00
gd_hle_state . next_request_id = 1 ;
2019-08-01 10:31:08 +00:00
gd_hle_state . last_request_id = r [ 0 ] = gd_hle_state . next_request_id + + ;
gd_hle_state . status = BIOS_ACTIVE ;
gd_hle_state . command = r [ 4 ] ;
2019-08-03 17:20:30 +00:00
gd_hle_state . multi_read_count = 0 ;
2019-08-01 10:31:08 +00:00
}
2019-07-30 17:04:51 +00:00
break ;
2014-12-29 21:05:35 +00:00
2019-07-30 17:04:51 +00:00
case GDROM_CHECK_COMMAND :
2019-08-01 10:31:08 +00:00
// Check if an enqueued command has completed.
//
// Args:
// r4 = request id
// r5 = pointer to four 32 bit integers to receive extended status information. The first is a generic error code.
//
// Returns:
// 0 - no such request active
// 1 - request is still being processed
// 2 - request has completed (if queried again, you will get a 0)
2019-08-03 17:20:30 +00:00
// 3 - multi request has data available
2019-08-01 10:31:08 +00:00
// -1 - request has failed (examine extended status information for cause of failure)
2019-08-08 06:24:13 +00:00
try {
WriteMem32 ( r [ 5 ] , gd_hle_state . result [ 0 ] ) ;
WriteMem32 ( r [ 5 ] + 4 , gd_hle_state . result [ 1 ] ) ;
WriteMem32 ( r [ 5 ] + 8 , gd_hle_state . result [ 2 ] ) ;
WriteMem32 ( r [ 5 ] + 12 , gd_hle_state . result [ 3 ] ) ;
} catch ( SH4ThrownException & ex ) {
}
2019-08-03 17:20:30 +00:00
if ( gd_hle_state . status = = BIOS_INACTIVE | | gd_hle_state . status = = BIOS_ACTIVE )
2019-08-01 10:31:08 +00:00
{
r [ 0 ] = gd_hle_state . status ; // no such request active or still being processed
}
else if ( r [ 4 ] ! = gd_hle_state . last_request_id )
2019-07-30 17:04:51 +00:00
{
2019-08-01 10:31:08 +00:00
r [ 0 ] = 0 ; // no such request active
2019-07-30 17:04:51 +00:00
}
else
{
2019-08-08 06:24:13 +00:00
if ( gd_hle_state . status = = BIOS_DATA_AVAIL & & gd_hle_state . command = = GDCC_REQ_PIO_TRANS )
r [ 0 ] = BIOS_ACTIVE ; // Bust-a-move 4 likes this
else
r [ 0 ] = gd_hle_state . status ; // completed or error
// Fixes NBA 2K
2019-08-04 17:09:02 +00:00
if ( gd_hle_state . status = = BIOS_DATA_AVAIL & & gd_hle_state . multi_read_count = = 0 )
{
gd_hle_state . status = BIOS_COMPLETED ;
gd_hle_state . result [ 3 ] = 0 ;
}
2019-08-08 06:24:13 +00:00
else if ( gd_hle_state . status ! = BIOS_DATA_AVAIL )
2019-08-03 17:20:30 +00:00
{
gd_hle_state . status = BIOS_INACTIVE ;
gd_hle_state . last_request_id = 0xFFFFFFFF ;
}
2019-07-30 17:04:51 +00:00
}
2019-08-08 06:24:13 +00:00
debugf ( " GDROM: HLE CHECK COMMAND REQID:%X param ptr: %X -> %X : %x %x %x %x " , r [ 4 ] , r [ 5 ] , r [ 0 ] ,
gd_hle_state . result [ 0 ] , gd_hle_state . result [ 1 ] , gd_hle_state . result [ 2 ] , gd_hle_state . result [ 3 ] ) ;
2019-07-30 17:04:51 +00:00
break ;
2014-12-29 21:05:35 +00:00
2019-07-30 17:04:51 +00:00
case GDROM_MAIN :
2019-08-01 10:31:08 +00:00
// In order for enqueued commands to get processed, this function must be called a few times.
2019-08-03 17:20:30 +00:00
debugf ( " GDROM: HLE GDROM_MAIN " ) ;
2019-08-04 17:09:02 +00:00
if ( gd_hle_state . status = = BIOS_ACTIVE | | ( gd_hle_state . status = = BIOS_DATA_AVAIL & & gd_hle_state . command = = GDCC_REQ_PIO_TRANS ) )
2019-08-01 10:31:08 +00:00
{
GD_HLE_Command ( gd_hle_state . command ) ;
}
2014-12-29 21:05:35 +00:00
break ;
2019-07-30 17:04:51 +00:00
case GDROM_INIT :
2019-08-01 10:31:08 +00:00
// Initialize the GDROM subsystem. Should be called before any requests are enqueued.
2019-09-19 09:36:59 +00:00
DEBUG_LOG ( REIOS , " GDROM: HLE GDROM_INIT " ) ;
2019-08-01 10:31:08 +00:00
gd_hle_state . last_request_id = 0xFFFFFFFF ;
2019-08-08 06:24:13 +00:00
gd_hle_state . next_request_id = 2 ;
2019-08-01 10:31:08 +00:00
gd_hle_state . status = BIOS_INACTIVE ;
2019-07-30 17:04:51 +00:00
break ;
2014-12-29 21:05:35 +00:00
2019-07-30 17:04:51 +00:00
case GDROM_RESET :
2019-08-01 10:31:08 +00:00
// Resets the drive.
2019-09-19 09:36:59 +00:00
DEBUG_LOG ( REIOS , " GDROM: HLE GDROM_RESET " ) ;
2019-08-01 10:31:08 +00:00
gd_hle_state . last_request_id = 0xFFFFFFFF ;
gd_hle_state . status = BIOS_INACTIVE ;
2019-07-30 17:04:51 +00:00
break ;
2014-12-29 21:05:35 +00:00
2019-07-30 17:04:51 +00:00
case GDROM_CHECK_DRIVE :
2020-01-30 17:59:26 +00:00
{
// Checks the general condition of the drive.
//
// Args:
// r4 = pointer to two 32 bit integers, to receive the drive status. The first is the current drive status, the second is the type of disc inserted (if any).
// 0 Drive is busy
// 1 Drive is paused
// 2 Drive is in standby
// 3 Drive is playing
// 4 Drive is seeking
// 5 Drive is scanning
// 6 Drive lid is open
// 7 Lid is closed, but there is no disc
//
// Returns: zero if successful, nonzero if failure
u32 discType = libGDR_GetDiscType ( ) ;
switch ( discType )
{
case Open :
WriteMem32 ( r [ 4 ] , 6 ) ;
WriteMem32 ( r [ 4 ] + 4 , 0 ) ;
break ;
case NoDisk :
WriteMem32 ( r [ 4 ] , 7 ) ;
WriteMem32 ( r [ 4 ] + 4 , 0 ) ;
break ;
default :
WriteMem32 ( r [ 4 ] , ( gd_hle_state . status = = BIOS_DATA_AVAIL | | SecNumber . Status = = GD_PLAY ) ? 3 : 1 ) ;
if ( memcmp ( ip_meta . disk_type , " GD-ROM " , sizeof ( ip_meta . disk_type ) ) = = 0 )
WriteMem32 ( r [ 4 ] + 4 , GdRom ) ;
else
WriteMem32 ( r [ 4 ] + 4 , discType ) ;
break ;
}
debugf ( " GDROM: HLE GDROM_CHECK_DRIVE r4:%X -> %x %x " , r [ 4 ] , ReadMem32 ( r [ 4 ] ) , ReadMem32 ( r [ 4 ] + 4 ) ) ;
r [ 0 ] = 0 ;
}
2019-07-30 17:04:51 +00:00
break ;
case GDROM_ABORT_COMMAND :
2019-08-01 10:31:08 +00:00
// Tries to abort a previously enqueued command.
//
// Args:
// r4 = request id
//
// Returns: zero if successful, nonzero if failure
2019-08-03 17:20:30 +00:00
WARN_LOG ( REIOS , " GDROM: HLE GDROM_ABORT_COMMAND r4:%X " , r [ 4 ] ) ;
2019-08-08 06:24:13 +00:00
if ( r [ 4 ] = = gd_hle_state . last_request_id
& & ( gd_hle_state . status = = BIOS_DATA_AVAIL | | gd_hle_state . status = = BIOS_ACTIVE ) )
2019-08-04 17:09:02 +00:00
{
r [ 0 ] = 0 ;
gd_hle_state . multi_read_count = 0 ;
2019-08-08 06:24:13 +00:00
gd_hle_state . xfer_end_time = 0 ;
2019-08-04 17:09:02 +00:00
}
else
{
r [ 0 ] = - 1 ;
}
2019-07-30 17:04:51 +00:00
break ;
2014-12-29 21:05:35 +00:00
2019-07-30 17:04:51 +00:00
case GDROM_SECTOR_MODE :
2019-08-01 10:31:08 +00:00
// Sets/gets the sector format for read commands.
//
// Args:
// r4 = pointer to a struct of four 32 bit integers containing new values, or to receive the old values
// Field Function
// 0 Get/Set, if 0 the mode will be set, if 1 it will be queried.
// 1 ? (always 8192)
// 2 1024 = mode 1, 2048 = mode 2, 0 = auto detect
// 3 Sector size in bytes (normally 2048)
//
// Returns: zero if successful, -1 if failure
2019-09-19 09:36:59 +00:00
DEBUG_LOG ( REIOS , " GDROM: HLE GDROM_SECTOR_MODE PTR_r4:%X " , r [ 4 ] ) ;
2014-12-29 21:05:35 +00:00
for ( int i = 0 ; i < 4 ; i + + ) {
SecMode [ i ] = ReadMem32 ( r [ 4 ] + ( i < < 2 ) ) ;
2019-09-19 09:36:59 +00:00
DEBUG_LOG ( REIOS , " %08X " , SecMode [ i ] ) ;
2014-12-29 21:05:35 +00:00
}
2019-08-01 10:31:08 +00:00
r [ 0 ] = 0 ;
2019-07-30 17:04:51 +00:00
break ;
2014-12-29 21:05:35 +00:00
2019-08-03 17:20:30 +00:00
case GDROM_G1_DMA_END :
2019-09-19 09:36:59 +00:00
DEBUG_LOG ( REIOS , " GDROM: G1_DMA_END callback %x arg %x " , r [ 4 ] , r [ 5 ] ) ;
2019-08-03 17:20:30 +00:00
gd_hle_state . multi_callback = r [ 4 ] ;
gd_hle_state . multi_callback_arg = r [ 5 ] ;
r [ 0 ] = 0 ;
2019-08-04 17:09:02 +00:00
if ( gd_hle_state . multi_callback ! = 0 & & gd_hle_state . dma_trans_ended ) // FIXME hack for 2K sports games
2019-08-03 17:20:30 +00:00
{
r [ 4 ] = gd_hle_state . multi_callback_arg ;
Sh4cntx . pc = gd_hle_state . multi_callback ;
2019-08-04 17:09:02 +00:00
gd_hle_state . dma_trans_ended = false ;
2019-08-03 17:20:30 +00:00
}
break ;
case GDROM_REQ_DMA_TRANS :
gd_hle_state . params [ 0 ] = ReadMem32 ( r [ 5 ] ) ;
gd_hle_state . params [ 1 ] = ReadMem32 ( r [ 5 ] + 4 ) ;
2019-09-19 09:36:59 +00:00
DEBUG_LOG ( REIOS , " GDROM: REQ_DMA_TRANS req_id %x dest %x size %x " ,
2019-08-03 17:20:30 +00:00
r [ 4 ] , gd_hle_state . params [ 0 ] , gd_hle_state . params [ 1 ] ) ;
2019-08-04 17:09:02 +00:00
if ( gd_hle_state . status ! = BIOS_DATA_AVAIL | | gd_hle_state . params [ 1 ] > gd_hle_state . multi_read_count )
{
r [ 0 ] = - 1 ;
}
else
{
multi_xfer < true > ( ) ;
r [ 0 ] = 0 ;
}
2019-08-03 17:20:30 +00:00
break ;
case GDROM_REQ_PIO_TRANS :
gd_hle_state . params [ 0 ] = ReadMem32 ( r [ 5 ] ) ;
gd_hle_state . params [ 1 ] = ReadMem32 ( r [ 5 ] + 4 ) ;
2019-09-19 09:36:59 +00:00
DEBUG_LOG ( REIOS , " GDROM: REQ_PIO_TRANS req_id %x dest %x size %x " ,
2019-08-03 17:20:30 +00:00
r [ 4 ] , gd_hle_state . params [ 0 ] , gd_hle_state . params [ 1 ] ) ;
2019-08-04 17:09:02 +00:00
if ( gd_hle_state . status ! = BIOS_DATA_AVAIL | | gd_hle_state . params [ 1 ] > gd_hle_state . multi_read_count )
{
r [ 0 ] = - 1 ;
}
else
{
gd_hle_state . command = GDCC_REQ_PIO_TRANS ;
r [ 0 ] = 0 ;
}
2019-08-03 17:20:30 +00:00
break ;
case GDROM_CHECK_DMA_TRANS :
{
u32 len_addr = r [ 5 ] ;
2019-09-19 09:36:59 +00:00
DEBUG_LOG ( REIOS , " GDROM: CHECK_DMA_TRANS req_id %x len_addr %x -> %x " , r [ 4 ] , len_addr , gd_hle_state . multi_read_count ) ;
2019-08-04 17:09:02 +00:00
if ( gd_hle_state . status = = BIOS_DATA_AVAIL )
{
WriteMem32 ( len_addr , gd_hle_state . multi_read_count ) ;
r [ 0 ] = 0 ;
}
else
{
r [ 0 ] = - 1 ;
}
2019-08-03 17:20:30 +00:00
}
break ;
case GDROM_SET_PIO_CALLBACK :
2019-09-19 09:36:59 +00:00
DEBUG_LOG ( REIOS , " GDROM: SET_PIO_CALLBACK callback %x arg %x " , r [ 4 ] , r [ 5 ] ) ;
2019-08-03 17:20:30 +00:00
gd_hle_state . multi_callback = r [ 4 ] ;
gd_hle_state . multi_callback_arg = r [ 5 ] ;
r [ 0 ] = 0 ;
break ;
case GDROM_CHECK_PIO_TRANS :
{
u32 len_addr = r [ 5 ] ;
2019-09-19 09:36:59 +00:00
DEBUG_LOG ( REIOS , " GDROM: CHECK_PIO_TRANS req_id %x len_addr %x -> %x " , r [ 4 ] , len_addr , gd_hle_state . multi_read_count ) ;
2019-08-04 17:09:02 +00:00
if ( gd_hle_state . status = = BIOS_DATA_AVAIL )
{
WriteMem32 ( len_addr , gd_hle_state . multi_read_count ) ;
r [ 0 ] = 0 ;
}
else
{
r [ 0 ] = - 1 ;
}
2019-08-03 17:20:30 +00:00
}
break ;
2019-07-30 17:04:51 +00:00
default :
WARN_LOG ( REIOS , " GDROM: Unknown SYSCALL: %X " , r [ 7 ] ) ;
break ;
2014-12-29 21:05:35 +00:00
}
}
else // MISC
{
2019-07-30 17:04:51 +00:00
switch ( r [ 7 ] )
{
case MISC_INIT :
2019-08-01 10:31:08 +00:00
// Initializes all the syscall vectors to their default values.
// Returns: zero
2019-07-30 17:04:51 +00:00
WARN_LOG ( REIOS , " GDROM: MISC_INIT not implemented " ) ;
2019-08-01 10:31:08 +00:00
r [ 0 ] = 0 ;
2019-07-30 17:04:51 +00:00
break ;
case MISC_SETVECTOR :
2019-08-01 10:31:08 +00:00
// Sets/clears the handler for one of the eight superfunctions for this vector. Setting a handler is only allowed if it not currently set.
//
// Args:
// r4 = superfunction number (0-7)
// r5 = pointer to handler function, or NULL to clear
//
// Returns: zero if successful, -1 if setting/clearing the handler fails
2019-07-30 17:04:51 +00:00
WARN_LOG ( REIOS , " GDROM: MISC_SETVECTOR not implemented " ) ;
break ;
default :
WARN_LOG ( REIOS , " GDROM: Unknown MISC command %x " , r [ 7 ] ) ;
break ;
}
2014-12-29 21:05:35 +00:00
}
2019-07-01 16:23:10 +00:00
}