mirror of
https://gitlab.freedesktop.org/mesa/drm.git
synced 2026-05-09 02:48:04 +02:00
VIA drm updates:
1. Improved security check of AGP texture adresses.
2. Hopefully last fix of ring-buffer jump oddities.
3. Added ioctl to check available space and command regulator lag in
ring-buffer. This is needed for 3D application responsiveness.
This commit is contained in:
parent
b4782ba76b
commit
aff0a25485
10 changed files with 380 additions and 116 deletions
|
|
@ -26,7 +26,7 @@
|
|||
*vb++ = (w2); \
|
||||
dev_priv->dma_low += 8;
|
||||
|
||||
#define PCI_BUF_SIZE 1024000
|
||||
#define PCI_BUF_SIZE 512000
|
||||
|
||||
static char pci_buf[PCI_BUF_SIZE];
|
||||
static unsigned long pci_bufsiz = PCI_BUF_SIZE;
|
||||
|
|
@ -37,6 +37,42 @@ static void via_cmdbuf_pause(drm_via_private_t * dev_priv);
|
|||
static void via_cmdbuf_reset(drm_via_private_t * dev_priv);
|
||||
static void via_cmdbuf_rewind(drm_via_private_t * dev_priv);
|
||||
|
||||
/*
|
||||
* Free space in command buffer.
|
||||
*/
|
||||
|
||||
static uint32_t
|
||||
via_cmdbuf_space(drm_via_private_t *dev_priv)
|
||||
{
|
||||
uint32_t agp_base = dev_priv->dma_offset +
|
||||
(uint32_t) dev_priv->agpAddr;
|
||||
uint32_t hw_addr = *(dev_priv->hw_addr_ptr) - agp_base;
|
||||
|
||||
return ((hw_addr <= dev_priv->dma_low) ?
|
||||
(dev_priv->dma_high + hw_addr - dev_priv->dma_low) :
|
||||
(hw_addr - dev_priv->dma_low));
|
||||
}
|
||||
|
||||
/*
|
||||
* How much does the command regulator lag behind?
|
||||
*/
|
||||
|
||||
static uint32_t
|
||||
via_cmdbuf_lag(drm_via_private_t *dev_priv)
|
||||
{
|
||||
uint32_t agp_base = dev_priv->dma_offset +
|
||||
(uint32_t) dev_priv->agpAddr;
|
||||
uint32_t hw_addr = *(dev_priv->hw_addr_ptr) - agp_base;
|
||||
|
||||
return ((hw_addr <= dev_priv->dma_low) ?
|
||||
(dev_priv->dma_low - hw_addr) :
|
||||
(dev_priv->dma_wrap + dev_priv->dma_low - hw_addr));
|
||||
}
|
||||
|
||||
/*
|
||||
* Check that the given size fits in the buffer, otherwise wait.
|
||||
*/
|
||||
|
||||
static inline int
|
||||
via_cmdbuf_wait(drm_via_private_t * dev_priv, unsigned int size)
|
||||
{
|
||||
|
|
@ -46,8 +82,8 @@ via_cmdbuf_wait(drm_via_private_t * dev_priv, unsigned int size)
|
|||
uint32_t count;
|
||||
hw_addr_ptr = dev_priv->hw_addr_ptr;
|
||||
cur_addr = dev_priv->dma_low;
|
||||
next_addr = cur_addr + size + 512*1024;
|
||||
count = 1000000; /* How long is this? */
|
||||
next_addr = cur_addr + size;
|
||||
count = 1000000;
|
||||
do {
|
||||
hw_addr = *hw_addr_ptr - agp_base;
|
||||
if (count-- == 0) {
|
||||
|
|
@ -59,12 +95,14 @@ via_cmdbuf_wait(drm_via_private_t * dev_priv, unsigned int size)
|
|||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Checks whether buffer head has reach the end. Rewind the ring buffer
|
||||
* when necessary.
|
||||
*
|
||||
* Returns virtual pointer to ring buffer.
|
||||
*/
|
||||
|
||||
static inline uint32_t *via_check_dma(drm_via_private_t * dev_priv,
|
||||
unsigned int size)
|
||||
{
|
||||
|
|
@ -131,6 +169,7 @@ static int via_initialize(drm_device_t * dev,
|
|||
dev_priv->dma_ptr = dev_priv->ring.virtual_start;
|
||||
dev_priv->dma_low = 0;
|
||||
dev_priv->dma_high = init->size;
|
||||
dev_priv->dma_wrap = init->size;
|
||||
dev_priv->dma_offset = init->offset;
|
||||
dev_priv->last_pause_ptr = NULL;
|
||||
dev_priv->hw_addr_ptr = dev_priv->mmio->handle + init->reg_pause_addr;
|
||||
|
|
@ -158,7 +197,7 @@ int via_dma_init(DRM_IOCTL_ARGS)
|
|||
retcode = via_dma_cleanup(dev);
|
||||
break;
|
||||
case VIA_DMA_INITIALIZED:
|
||||
retcode = (dev_priv->ring.virtual_start != NULL) ? 0: DRM_ERR( EFAULT );
|
||||
retcode = (dev_priv->ring.virtual_start != NULL) ? 0: DRM_ERR( EFAULT );
|
||||
break;
|
||||
default:
|
||||
retcode = DRM_ERR(EINVAL);
|
||||
|
|
@ -348,11 +387,11 @@ int via_pci_cmdbuffer(DRM_IOCTL_ARGS)
|
|||
#define VIA_3D_ENG_BUSY 0x00000002 /* 3D Engine is busy */
|
||||
#define VIA_VR_QUEUE_BUSY 0x00020000 /* Virtual Queue is busy */
|
||||
|
||||
#define SetReg2DAGP(nReg, nData) { \
|
||||
*((uint32_t *)(vb)) = ((nReg) >> 2) | HALCYON_HEADER1; \
|
||||
*((uint32_t *)(vb) + 1) = (nData); \
|
||||
vb = ((uint32_t *)vb) + 2; \
|
||||
dev_priv->dma_low +=8; \
|
||||
#define SetReg2DAGP(nReg, nData) { \
|
||||
*((uint32_t *)(vb)) = ((nReg) >> 2) | HALCYON_HEADER1; \
|
||||
*((uint32_t *)(vb) + 1) = (nData); \
|
||||
vb = ((uint32_t *)vb) + 2; \
|
||||
dev_priv->dma_low +=8; \
|
||||
}
|
||||
|
||||
static inline uint32_t *via_align_buffer(drm_via_private_t * dev_priv,
|
||||
|
|
@ -447,7 +486,7 @@ static uint32_t *via_align_cmd(drm_via_private_t * dev_priv, uint32_t cmd_type,
|
|||
uint32_t qw_pad_count;
|
||||
|
||||
if (!skip_wait)
|
||||
via_cmdbuf_wait(dev_priv, 2*CMDBUF_ALIGNMENT_SIZE);
|
||||
via_cmdbuf_wait(dev_priv, 2*CMDBUF_ALIGNMENT_SIZE);
|
||||
|
||||
vb = via_get_dma(dev_priv);
|
||||
VIA_OUT_RING_QW( HC_HEADER2 | ((VIA_REG_TRANSET >> 2) << 12) |
|
||||
|
|
@ -509,6 +548,7 @@ static void via_cmdbuf_start(drm_via_private_t * dev_priv)
|
|||
|
||||
VIA_WRITE(VIA_REG_TRANSPACE, command | HC_HAGPCMNT_MASK);
|
||||
}
|
||||
|
||||
static inline void via_dummy_bitblt(drm_via_private_t * dev_priv)
|
||||
{
|
||||
uint32_t *vb = via_get_dma(dev_priv);
|
||||
|
|
@ -524,37 +564,53 @@ static void via_cmdbuf_jump(drm_via_private_t * dev_priv)
|
|||
uint32_t pause_addr_lo, pause_addr_hi;
|
||||
uint32_t jump_addr_lo, jump_addr_hi;
|
||||
volatile uint32_t *last_pause_ptr;
|
||||
uint32_t dma_low_save1, dma_low_save2;
|
||||
|
||||
agp_base = dev_priv->dma_offset + (uint32_t) dev_priv->agpAddr;
|
||||
via_align_cmd(dev_priv, HC_HAGPBpID_JUMP, 0, &jump_addr_hi,
|
||||
&jump_addr_lo, 0);
|
||||
|
||||
dev_priv->dma_wrap = dev_priv->dma_low;
|
||||
|
||||
|
||||
/*
|
||||
* Wrap command buffer to the beginning.
|
||||
*/
|
||||
|
||||
dev_priv->dma_low = 0;
|
||||
if (via_cmdbuf_wait(dev_priv, CMDBUF_ALIGNMENT_SIZE) != 0) {
|
||||
DRM_ERROR("via_cmdbuf_jump failed\n");
|
||||
}
|
||||
|
||||
/*
|
||||
* The command regulator needs to stall for a while since it probably
|
||||
* had a concussion during the jump... It will stall at the second
|
||||
* bitblt since the 2D engine is busy with the first.
|
||||
*/
|
||||
|
||||
via_dummy_bitblt(dev_priv);
|
||||
via_dummy_bitblt(dev_priv);
|
||||
last_pause_ptr = via_align_cmd(dev_priv, HC_HAGPBpID_PAUSE, 0, &pause_addr_hi,
|
||||
&pause_addr_lo, 0) -1;
|
||||
|
||||
/*
|
||||
* The regulator may still be suffering from the shock of the jump.
|
||||
* Add another pause command to make sure it really will get itself together
|
||||
* and pause.
|
||||
*/
|
||||
|
||||
via_align_cmd(dev_priv, HC_HAGPBpID_PAUSE, 0, &pause_addr_hi,
|
||||
&pause_addr_lo, 0);
|
||||
*last_pause_ptr = pause_addr_lo;
|
||||
|
||||
*last_pause_ptr = pause_addr_lo;
|
||||
dma_low_save1 = dev_priv->dma_low;
|
||||
|
||||
/*
|
||||
* Now, set a trap that will pause the regulator if it tries to rerun the old
|
||||
* command buffer. (Which may happen if via_hook_segment detecs a command regulator pause
|
||||
* and reissues the jump command over PCI, while the regulator has already taken the jump
|
||||
* and actually paused at the current buffer end).
|
||||
* There appears to be no other way to detect this condition, since the hw_addr_pointer
|
||||
* does not seem to get updated immediately when a jump occurs.
|
||||
*/
|
||||
|
||||
last_pause_ptr = via_align_cmd(dev_priv, HC_HAGPBpID_PAUSE, 0, &pause_addr_hi,
|
||||
&pause_addr_lo, 0) -1;
|
||||
via_align_cmd(dev_priv, HC_HAGPBpID_PAUSE, 0, &pause_addr_hi,
|
||||
&pause_addr_lo, 0);
|
||||
*last_pause_ptr = pause_addr_lo;
|
||||
|
||||
dma_low_save2 = dev_priv->dma_low;
|
||||
dev_priv->dma_low = dma_low_save1;
|
||||
via_hook_segment( dev_priv, jump_addr_hi, jump_addr_lo, 0);
|
||||
dev_priv->dma_low = dma_low_save2;
|
||||
via_hook_segment( dev_priv, pause_addr_hi, pause_addr_lo, 0);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -583,4 +639,64 @@ static void via_cmdbuf_reset(drm_via_private_t * dev_priv)
|
|||
via_wait_idle(dev_priv);
|
||||
}
|
||||
|
||||
/************************************************************************/
|
||||
/*
|
||||
* User interface to the space and lag function.
|
||||
*/
|
||||
|
||||
int
|
||||
via_cmdbuf_size(DRM_IOCTL_ARGS)
|
||||
{
|
||||
DRM_DEVICE;
|
||||
drm_via_cmdbuf_size_t d_siz;
|
||||
int ret = 0;
|
||||
uint32_t tmp_size, count;
|
||||
drm_via_private_t *dev_priv;
|
||||
|
||||
DRM_DEBUG("via cmdbuf_size\n");
|
||||
LOCK_TEST_WITH_RETURN( dev, filp );
|
||||
|
||||
dev_priv = (drm_via_private_t *) dev->dev_private;
|
||||
|
||||
if (dev_priv->ring.virtual_start == NULL) {
|
||||
DRM_ERROR("%s called without initializing AGP ring buffer.\n",
|
||||
__FUNCTION__);
|
||||
return DRM_ERR(EFAULT);
|
||||
}
|
||||
|
||||
DRM_COPY_FROM_USER_IOCTL(d_siz, (drm_via_cmdbuffer_t *) data,
|
||||
sizeof(d_siz));
|
||||
|
||||
|
||||
count = 1000000;
|
||||
tmp_size = d_siz.size;
|
||||
switch(d_siz.func) {
|
||||
case VIA_CMDBUF_SPACE:
|
||||
while (count-- && ((tmp_size = via_cmdbuf_space(dev_priv)) < d_siz.size)) {
|
||||
if (!d_siz.wait) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!count) {
|
||||
DRM_ERROR("VIA_CMDBUF_SPACE timed out.\n");
|
||||
ret = DRM_ERR(EAGAIN);
|
||||
}
|
||||
break;
|
||||
case VIA_CMDBUF_LAG:
|
||||
while (count-- && ((tmp_size = via_cmdbuf_lag(dev_priv)) > d_siz.size)) {
|
||||
if (!d_siz.wait) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!count) {
|
||||
DRM_ERROR("VIA_CMDBUF_SPACE timed out.\n");
|
||||
ret = DRM_ERR(EAGAIN);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return DRM_ERR(EFAULT);
|
||||
}
|
||||
d_siz.size = tmp_size;
|
||||
DRM_COPY_TO_USER_IOCTL((drm_via_cmdbuffer_t *) data, d_siz,
|
||||
sizeof(d_siz));
|
||||
return ret;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,17 +71,20 @@
|
|||
#define DRM_VIA_CMDBUFFER 0x08
|
||||
#define DRM_VIA_FLUSH 0x09
|
||||
#define DRM_VIA_PCICMD 0x0a
|
||||
#define DRM_VIA_CMDBUF_SIZE 0x0b
|
||||
|
||||
#define DRM_IOCTL_VIA_ALLOCMEM DRM_IOWR(DRM_COMMAND_BASE + DRM_VIA_ALLOCMEM, drm_via_mem_t)
|
||||
#define DRM_IOCTL_VIA_FREEMEM DRM_IOW( DRM_COMMAND_BASE + DRM_VIA_FREEMEM, drm_via_mem_t)
|
||||
#define DRM_IOCTL_VIA_AGP_INIT DRM_IOWR(DRM_COMMAND_BASE + DRM_VIA_AGP_INIT, drm_via_agp_t)
|
||||
#define DRM_IOCTL_VIA_FB_INIT DRM_IOWR(DRM_COMMAND_BASE + DRM_VIA_FB_INIT, drm_via_fb_t)
|
||||
#define DRM_IOCTL_VIA_MAP_INIT DRM_IOWR(DRM_COMMAND_BASE + DRM_VIA_MAP_INIT, drm_via_init_t)
|
||||
#define DRM_IOCTL_VIA_DEC_FUTEX DRM_IOW( DRM_COMMAND_BASE + DRM_VIA_DEC_FUTEX, drm_via_futex_t)
|
||||
#define DRM_IOCTL_VIA_DMA_INIT DRM_IOWR(DRM_COMMAND_BASE + DRM_VIA_DMA_INIT, drm_via_dma_init_t)
|
||||
#define DRM_IOCTL_VIA_CMDBUFFER DRM_IOW( DRM_COMMAND_BASE + DRM_VIA_CMDBUFFER, drm_via_cmdbuffer_t)
|
||||
#define DRM_IOCTL_VIA_FLUSH DRM_IO( DRM_COMMAND_BASE + DRM_VIA_FLUSH)
|
||||
#define DRM_IOCTL_VIA_PCICMD DRM_IOW( DRM_COMMAND_BASE + DRM_VIA_PCICMD, drm_via_cmdbuffer_t)
|
||||
#define DRM_IOCTL_VIA_ALLOCMEM DRM_IOWR(DRM_COMMAND_BASE + DRM_VIA_ALLOCMEM, drm_via_mem_t)
|
||||
#define DRM_IOCTL_VIA_FREEMEM DRM_IOW( DRM_COMMAND_BASE + DRM_VIA_FREEMEM, drm_via_mem_t)
|
||||
#define DRM_IOCTL_VIA_AGP_INIT DRM_IOWR(DRM_COMMAND_BASE + DRM_VIA_AGP_INIT, drm_via_agp_t)
|
||||
#define DRM_IOCTL_VIA_FB_INIT DRM_IOWR(DRM_COMMAND_BASE + DRM_VIA_FB_INIT, drm_via_fb_t)
|
||||
#define DRM_IOCTL_VIA_MAP_INIT DRM_IOWR(DRM_COMMAND_BASE + DRM_VIA_MAP_INIT, drm_via_init_t)
|
||||
#define DRM_IOCTL_VIA_DEC_FUTEX DRM_IOW( DRM_COMMAND_BASE + DRM_VIA_DEC_FUTEX, drm_via_futex_t)
|
||||
#define DRM_IOCTL_VIA_DMA_INIT DRM_IOWR(DRM_COMMAND_BASE + DRM_VIA_DMA_INIT, drm_via_dma_init_t)
|
||||
#define DRM_IOCTL_VIA_CMDBUFFER DRM_IOW( DRM_COMMAND_BASE + DRM_VIA_CMDBUFFER, drm_via_cmdbuffer_t)
|
||||
#define DRM_IOCTL_VIA_FLUSH DRM_IO( DRM_COMMAND_BASE + DRM_VIA_FLUSH)
|
||||
#define DRM_IOCTL_VIA_PCICMD DRM_IOW( DRM_COMMAND_BASE + DRM_VIA_PCICMD, drm_via_cmdbuffer_t)
|
||||
#define DRM_IOCTL_VIA_CMDBUF_SIZE DRM_IOWR( DRM_COMMAND_BASE + DRM_VIA_CMDBUF_SIZE, \
|
||||
drm_via_cmdbuf_size_t)
|
||||
|
||||
/* Indices into buf.Setup where various bits of state are mirrored per
|
||||
* context and per buffer. These can be fired at the card as a unit,
|
||||
|
|
@ -187,20 +190,14 @@ typedef struct _drm_via_sarea {
|
|||
|
||||
} drm_via_sarea_t;
|
||||
|
||||
typedef struct _drm_via_flush_agp {
|
||||
unsigned int offset;
|
||||
unsigned int size;
|
||||
unsigned int index;
|
||||
int discard; /* client is finished with the buffer? */
|
||||
} drm_via_flush_agp_t;
|
||||
|
||||
typedef struct _drm_via_flush_sys {
|
||||
unsigned int offset;
|
||||
unsigned int size;
|
||||
unsigned long index;
|
||||
int discard; /* client is finished with the buffer? */
|
||||
} drm_via_flush_sys_t;
|
||||
|
||||
typedef struct _drm_via_cmdbuf_size {
|
||||
enum {
|
||||
VIA_CMDBUF_SPACE = 0x01,
|
||||
VIA_CMDBUF_LAG = 0x02
|
||||
} func;
|
||||
int wait;
|
||||
uint32_t size;
|
||||
} drm_via_cmdbuf_size_t;
|
||||
|
||||
|
||||
#ifdef __KERNEL__
|
||||
|
|
@ -215,6 +212,7 @@ int via_dma_init(DRM_IOCTL_ARGS);
|
|||
int via_cmdbuffer(DRM_IOCTL_ARGS);
|
||||
int via_flush_ioctl(DRM_IOCTL_ARGS);
|
||||
int via_pci_cmdbuffer(DRM_IOCTL_ARGS);
|
||||
int via_cmdbuf_size(DRM_IOCTL_ARGS);
|
||||
|
||||
#endif
|
||||
#endif /* _VIA_DRM_H_ */
|
||||
|
|
|
|||
|
|
@ -68,7 +68,8 @@ static drm_ioctl_desc_t ioctls[] = {
|
|||
[DRM_IOCTL_NR(DRM_VIA_DMA_INIT)] = {via_dma_init, 1, 0},
|
||||
[DRM_IOCTL_NR(DRM_VIA_CMDBUFFER)] = {via_cmdbuffer, 1, 0},
|
||||
[DRM_IOCTL_NR(DRM_VIA_FLUSH)] = {via_flush_ioctl, 1, 0},
|
||||
[DRM_IOCTL_NR(DRM_VIA_PCICMD)] = {via_pci_cmdbuffer, 1, 0}
|
||||
[DRM_IOCTL_NR(DRM_VIA_PCICMD)] = {via_pci_cmdbuffer, 1, 0},
|
||||
[DRM_IOCTL_NR(DRM_VIA_CMDBUF_SIZE)] = {via_cmdbuf_size, 1, 0}
|
||||
};
|
||||
|
||||
static int probe(struct pci_dev *pdev, const struct pci_device_id *ent);
|
||||
|
|
|
|||
|
|
@ -28,11 +28,11 @@
|
|||
|
||||
#define DRIVER_NAME "via"
|
||||
#define DRIVER_DESC "VIA Unichrome"
|
||||
#define DRIVER_DATE "20041206"
|
||||
#define DRIVER_DATE "20041212"
|
||||
|
||||
#define DRIVER_MAJOR 2
|
||||
#define DRIVER_MINOR 2
|
||||
#define DRIVER_PATCHLEVEL 1
|
||||
#define DRIVER_MINOR 3
|
||||
#define DRIVER_PATCHLEVEL 0
|
||||
|
||||
typedef struct drm_via_ring_buffer {
|
||||
drm_map_t map;
|
||||
|
|
@ -50,6 +50,7 @@ typedef struct drm_via_private {
|
|||
unsigned int dma_low;
|
||||
unsigned int dma_high;
|
||||
unsigned int dma_offset;
|
||||
uint32_t dma_wrap;
|
||||
volatile uint32_t *last_pause_ptr;
|
||||
volatile uint32_t *hw_addr_ptr;
|
||||
drm_via_ring_buffer_t ring;
|
||||
|
|
|
|||
|
|
@ -253,39 +253,48 @@ typedef struct{
|
|||
sequence_t unfinished;
|
||||
int agp_texture;
|
||||
drm_device_t *dev;
|
||||
drm_map_t *map_cache;
|
||||
} sequence_context_t;
|
||||
|
||||
static sequence_context_t hc_sequence;
|
||||
|
||||
/*
|
||||
* stolen from drm_memory.h
|
||||
* Partially stolen from drm_memory.h
|
||||
*/
|
||||
|
||||
static __inline__ drm_map_t *
|
||||
via_drm_lookup_map (unsigned long offset, unsigned long size, drm_device_t *dev)
|
||||
via_drm_lookup_agp_map (sequence_context_t *seq, unsigned long offset, unsigned long size,
|
||||
drm_device_t *dev)
|
||||
{
|
||||
struct list_head *list;
|
||||
drm_map_list_t *r_list;
|
||||
drm_map_t *map;
|
||||
drm_map_t *map = seq->map_cache;
|
||||
|
||||
if (map && map->offset <= offset && (offset + size) <= (map->offset + map->size)) {
|
||||
return map;
|
||||
}
|
||||
|
||||
list_for_each(list, &dev->maplist->head) {
|
||||
r_list = (drm_map_list_t *) list;
|
||||
map = r_list->map;
|
||||
if (!map)
|
||||
continue;
|
||||
if (map->offset <= offset && (offset + size) <= (map->offset + map->size))
|
||||
if (map->offset <= offset && (offset + size) <= (map->offset + map->size) &&
|
||||
!(map->flags & _DRM_RESTRICTED) && (map->type == _DRM_AGP)) {
|
||||
seq->map_cache = map;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Require that all AGP texture levels reside in the same map which should
|
||||
* be mapped by the client. This is not a big restriction.
|
||||
* FIXME: To actually enforce this security policy strictly, drm_unmap
|
||||
* would have to wait for dma quiescent before unmapping an AGP page.
|
||||
* The via_drm_lookup_map call in reality seems to take
|
||||
* Require that all AGP texture levels reside in the same AGP map which should
|
||||
* be mappable by the client. This is not a big restriction.
|
||||
* FIXME: To actually enforce this security policy strictly, drm_rmmap
|
||||
* would have to wait for dma quiescent before removing an AGP map.
|
||||
* The via_drm_lookup_agp_map call in reality seems to take
|
||||
* very little CPU time.
|
||||
*/
|
||||
|
||||
|
|
@ -322,8 +331,8 @@ finish_current_sequence(sequence_context_t *cur_seq)
|
|||
if (tmp > hi) hi = tmp;
|
||||
}
|
||||
|
||||
if (! via_drm_lookup_map (lo, hi - lo, cur_seq->dev)) {
|
||||
DRM_ERROR("AGP texture is not in client map\n");
|
||||
if (! via_drm_lookup_agp_map (cur_seq, lo, hi - lo, cur_seq->dev)) {
|
||||
DRM_ERROR("AGP texture is not in allowed map\n");
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
|
@ -619,6 +628,7 @@ via_verify_command_stream(const uint32_t * buf, unsigned int size, drm_device_t
|
|||
|
||||
hc_sequence.dev = dev;
|
||||
hc_sequence.unfinished = no_sequence;
|
||||
hc_sequence.map_cache = NULL;
|
||||
|
||||
while (buf < buf_end) {
|
||||
switch (state) {
|
||||
|
|
|
|||
24
shared/via.h
24
shared/via.h
|
|
@ -30,22 +30,24 @@
|
|||
|
||||
#define DRIVER_NAME "via"
|
||||
#define DRIVER_DESC "VIA Unichrome"
|
||||
#define DRIVER_DATE "20041206"
|
||||
#define DRIVER_DATE "20041212"
|
||||
|
||||
#define DRIVER_MAJOR 2
|
||||
#define DRIVER_MINOR 2
|
||||
#define DRIVER_PATCHLEVEL 1
|
||||
#define DRIVER_MINOR 3
|
||||
#define DRIVER_PATCHLEVEL 0
|
||||
|
||||
#define DRIVER_IOCTLS \
|
||||
[DRM_IOCTL_NR(DRM_IOCTL_VIA_ALLOCMEM)] = { via_mem_alloc, 1, 0 }, \
|
||||
[DRM_IOCTL_NR(DRM_IOCTL_VIA_FREEMEM)] = { via_mem_free, 1, 0 }, \
|
||||
[DRM_IOCTL_NR(DRM_IOCTL_VIA_AGP_INIT)] = { via_agp_init, 1, 0 }, \
|
||||
[DRM_IOCTL_NR(DRM_IOCTL_VIA_FB_INIT)] = { via_fb_init, 1, 0 }, \
|
||||
[DRM_IOCTL_NR(DRM_IOCTL_VIA_MAP_INIT)] = { via_map_init, 1, 0 }, \
|
||||
[DRM_IOCTL_NR(DRM_IOCTL_VIA_DEC_FUTEX)] = { via_decoder_futex, 1, 0}, \
|
||||
[DRM_IOCTL_NR(DRM_IOCTL_VIA_DMA_INIT)] = { via_dma_init, 1, 0}, \
|
||||
[DRM_IOCTL_NR(DRM_IOCTL_VIA_CMDBUFFER)] = { via_cmdbuffer, 1, 0}, \
|
||||
[DRM_IOCTL_NR(DRM_IOCTL_VIA_FREEMEM)] = { via_mem_free, 1, 0 }, \
|
||||
[DRM_IOCTL_NR(DRM_IOCTL_VIA_AGP_INIT)] = { via_agp_init, 1, 0 }, \
|
||||
[DRM_IOCTL_NR(DRM_IOCTL_VIA_FB_INIT)] = { via_fb_init, 1, 0 }, \
|
||||
[DRM_IOCTL_NR(DRM_IOCTL_VIA_MAP_INIT)] = { via_map_init, 1, 0 }, \
|
||||
[DRM_IOCTL_NR(DRM_IOCTL_VIA_DEC_FUTEX)] = { via_decoder_futex, 1, 0}, \
|
||||
[DRM_IOCTL_NR(DRM_IOCTL_VIA_DMA_INIT)] = { via_dma_init, 1, 0}, \
|
||||
[DRM_IOCTL_NR(DRM_IOCTL_VIA_CMDBUFFER)] = { via_cmdbuffer, 1, 0}, \
|
||||
[DRM_IOCTL_NR(DRM_IOCTL_VIA_FLUSH)] = { via_flush_ioctl, 1, 0}, \
|
||||
[DRM_IOCTL_NR(DRM_IOCTL_VIA_PCICMD)] = { via_pci_cmdbuffer, 1, 0}
|
||||
[DRM_IOCTL_NR(DRM_IOCTL_VIA_PCICMD)] = { via_pci_cmdbuffer, 1, 0}, \
|
||||
[DRM_IOCTL_NR(DRM_IOCTL_VIA_CMDBUF_SIZE)] = {via_cmdbuf_size, 1, 0}
|
||||
|
||||
|
||||
#endif
|
||||
|
|
|
|||
171
shared/via_dma.c
171
shared/via_dma.c
|
|
@ -27,7 +27,7 @@
|
|||
*vb++ = (w2); \
|
||||
dev_priv->dma_low += 8;
|
||||
|
||||
#define PCI_BUF_SIZE 1024000
|
||||
#define PCI_BUF_SIZE 512000
|
||||
|
||||
static char pci_buf[PCI_BUF_SIZE];
|
||||
static unsigned long pci_bufsiz = PCI_BUF_SIZE;
|
||||
|
|
@ -38,6 +38,42 @@ static void via_cmdbuf_pause(drm_via_private_t * dev_priv);
|
|||
static void via_cmdbuf_reset(drm_via_private_t * dev_priv);
|
||||
static void via_cmdbuf_rewind(drm_via_private_t * dev_priv);
|
||||
|
||||
/*
|
||||
* Free space in command buffer.
|
||||
*/
|
||||
|
||||
static uint32_t
|
||||
via_cmdbuf_space(drm_via_private_t *dev_priv)
|
||||
{
|
||||
uint32_t agp_base = dev_priv->dma_offset +
|
||||
(uint32_t) dev_priv->agpAddr;
|
||||
uint32_t hw_addr = *(dev_priv->hw_addr_ptr) - agp_base;
|
||||
|
||||
return ((hw_addr <= dev_priv->dma_low) ?
|
||||
(dev_priv->dma_high + hw_addr - dev_priv->dma_low) :
|
||||
(hw_addr - dev_priv->dma_low));
|
||||
}
|
||||
|
||||
/*
|
||||
* How much does the command regulator lag behind?
|
||||
*/
|
||||
|
||||
static uint32_t
|
||||
via_cmdbuf_lag(drm_via_private_t *dev_priv)
|
||||
{
|
||||
uint32_t agp_base = dev_priv->dma_offset +
|
||||
(uint32_t) dev_priv->agpAddr;
|
||||
uint32_t hw_addr = *(dev_priv->hw_addr_ptr) - agp_base;
|
||||
|
||||
return ((hw_addr <= dev_priv->dma_low) ?
|
||||
(dev_priv->dma_low - hw_addr) :
|
||||
(dev_priv->dma_wrap + dev_priv->dma_low - hw_addr));
|
||||
}
|
||||
|
||||
/*
|
||||
* Check that the given size fits in the buffer, otherwise wait.
|
||||
*/
|
||||
|
||||
static inline int
|
||||
via_cmdbuf_wait(drm_via_private_t * dev_priv, unsigned int size)
|
||||
{
|
||||
|
|
@ -47,8 +83,8 @@ via_cmdbuf_wait(drm_via_private_t * dev_priv, unsigned int size)
|
|||
uint32_t count;
|
||||
hw_addr_ptr = dev_priv->hw_addr_ptr;
|
||||
cur_addr = dev_priv->dma_low;
|
||||
next_addr = cur_addr + size + 512*1024;
|
||||
count = 1000000; /* How long is this? */
|
||||
next_addr = cur_addr + size;
|
||||
count = 1000000;
|
||||
do {
|
||||
hw_addr = *hw_addr_ptr - agp_base;
|
||||
if (count-- == 0) {
|
||||
|
|
@ -60,12 +96,14 @@ via_cmdbuf_wait(drm_via_private_t * dev_priv, unsigned int size)
|
|||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Checks whether buffer head has reach the end. Rewind the ring buffer
|
||||
* when necessary.
|
||||
*
|
||||
* Returns virtual pointer to ring buffer.
|
||||
*/
|
||||
|
||||
static inline uint32_t *via_check_dma(drm_via_private_t * dev_priv,
|
||||
unsigned int size)
|
||||
{
|
||||
|
|
@ -132,6 +170,7 @@ static int via_initialize(drm_device_t * dev,
|
|||
dev_priv->dma_ptr = dev_priv->ring.virtual_start;
|
||||
dev_priv->dma_low = 0;
|
||||
dev_priv->dma_high = init->size;
|
||||
dev_priv->dma_wrap = init->size;
|
||||
dev_priv->dma_offset = init->offset;
|
||||
dev_priv->last_pause_ptr = NULL;
|
||||
dev_priv->hw_addr_ptr = dev_priv->mmio->handle + init->reg_pause_addr;
|
||||
|
|
@ -159,7 +198,7 @@ int via_dma_init(DRM_IOCTL_ARGS)
|
|||
retcode = via_dma_cleanup(dev);
|
||||
break;
|
||||
case VIA_DMA_INITIALIZED:
|
||||
retcode = (dev_priv->ring.virtual_start != NULL) ? 0: DRM_ERR( EFAULT );
|
||||
retcode = (dev_priv->ring.virtual_start != NULL) ? 0: DRM_ERR( EFAULT );
|
||||
break;
|
||||
default:
|
||||
retcode = DRM_ERR(EINVAL);
|
||||
|
|
@ -171,11 +210,10 @@ int via_dma_init(DRM_IOCTL_ARGS)
|
|||
|
||||
static int via_dispatch_cmdbuffer(drm_device_t * dev, drm_via_cmdbuffer_t * cmd)
|
||||
{
|
||||
drm_via_private_t *dev_priv;
|
||||
drm_via_private_t *dev_priv;
|
||||
uint32_t *vb;
|
||||
int ret;
|
||||
|
||||
|
||||
dev_priv = (drm_via_private_t *) dev->dev_private;
|
||||
|
||||
if (dev_priv->ring.virtual_start == NULL) {
|
||||
|
|
@ -350,11 +388,11 @@ int via_pci_cmdbuffer(DRM_IOCTL_ARGS)
|
|||
#define VIA_3D_ENG_BUSY 0x00000002 /* 3D Engine is busy */
|
||||
#define VIA_VR_QUEUE_BUSY 0x00020000 /* Virtual Queue is busy */
|
||||
|
||||
#define SetReg2DAGP(nReg, nData) { \
|
||||
*((uint32_t *)(vb)) = ((nReg) >> 2) | HALCYON_HEADER1; \
|
||||
*((uint32_t *)(vb) + 1) = (nData); \
|
||||
vb = ((uint32_t *)vb) + 2; \
|
||||
dev_priv->dma_low +=8; \
|
||||
#define SetReg2DAGP(nReg, nData) { \
|
||||
*((uint32_t *)(vb)) = ((nReg) >> 2) | HALCYON_HEADER1; \
|
||||
*((uint32_t *)(vb) + 1) = (nData); \
|
||||
vb = ((uint32_t *)vb) + 2; \
|
||||
dev_priv->dma_low +=8; \
|
||||
}
|
||||
|
||||
static inline uint32_t *via_align_buffer(drm_via_private_t * dev_priv,
|
||||
|
|
@ -449,7 +487,7 @@ static uint32_t *via_align_cmd(drm_via_private_t * dev_priv, uint32_t cmd_type,
|
|||
uint32_t qw_pad_count;
|
||||
|
||||
if (!skip_wait)
|
||||
via_cmdbuf_wait(dev_priv, 2*CMDBUF_ALIGNMENT_SIZE);
|
||||
via_cmdbuf_wait(dev_priv, 2*CMDBUF_ALIGNMENT_SIZE);
|
||||
|
||||
vb = via_get_dma(dev_priv);
|
||||
VIA_OUT_RING_QW( HC_HEADER2 | ((VIA_REG_TRANSET >> 2) << 12) |
|
||||
|
|
@ -511,6 +549,7 @@ static void via_cmdbuf_start(drm_via_private_t * dev_priv)
|
|||
|
||||
VIA_WRITE(VIA_REG_TRANSPACE, command | HC_HAGPCMNT_MASK);
|
||||
}
|
||||
|
||||
static inline void via_dummy_bitblt(drm_via_private_t * dev_priv)
|
||||
{
|
||||
uint32_t *vb = via_get_dma(dev_priv);
|
||||
|
|
@ -526,37 +565,53 @@ static void via_cmdbuf_jump(drm_via_private_t * dev_priv)
|
|||
uint32_t pause_addr_lo, pause_addr_hi;
|
||||
uint32_t jump_addr_lo, jump_addr_hi;
|
||||
volatile uint32_t *last_pause_ptr;
|
||||
uint32_t dma_low_save1, dma_low_save2;
|
||||
|
||||
agp_base = dev_priv->dma_offset + (uint32_t) dev_priv->agpAddr;
|
||||
via_align_cmd(dev_priv, HC_HAGPBpID_JUMP, 0, &jump_addr_hi,
|
||||
&jump_addr_lo, 0);
|
||||
|
||||
dev_priv->dma_wrap = dev_priv->dma_low;
|
||||
|
||||
|
||||
/*
|
||||
* Wrap command buffer to the beginning.
|
||||
*/
|
||||
|
||||
dev_priv->dma_low = 0;
|
||||
if (via_cmdbuf_wait(dev_priv, CMDBUF_ALIGNMENT_SIZE) != 0) {
|
||||
DRM_ERROR("via_cmdbuf_jump failed\n");
|
||||
}
|
||||
|
||||
/*
|
||||
* The command regulator needs to stall for a while since it probably
|
||||
* had a concussion during the jump... It will stall at the second
|
||||
* bitblt since the 2D engine is busy with the first.
|
||||
*/
|
||||
|
||||
via_dummy_bitblt(dev_priv);
|
||||
via_dummy_bitblt(dev_priv);
|
||||
last_pause_ptr = via_align_cmd(dev_priv, HC_HAGPBpID_PAUSE, 0, &pause_addr_hi,
|
||||
&pause_addr_lo, 0) -1;
|
||||
|
||||
/*
|
||||
* The regulator may still be suffering from the shock of the jump.
|
||||
* Add another pause command to make sure it really will get itself together
|
||||
* and pause.
|
||||
*/
|
||||
|
||||
via_align_cmd(dev_priv, HC_HAGPBpID_PAUSE, 0, &pause_addr_hi,
|
||||
&pause_addr_lo, 0);
|
||||
*last_pause_ptr = pause_addr_lo;
|
||||
|
||||
*last_pause_ptr = pause_addr_lo;
|
||||
dma_low_save1 = dev_priv->dma_low;
|
||||
|
||||
/*
|
||||
* Now, set a trap that will pause the regulator if it tries to rerun the old
|
||||
* command buffer. (Which may happen if via_hook_segment detecs a command regulator pause
|
||||
* and reissues the jump command over PCI, while the regulator has already taken the jump
|
||||
* and actually paused at the current buffer end).
|
||||
* There appears to be no other way to detect this condition, since the hw_addr_pointer
|
||||
* does not seem to get updated immediately when a jump occurs.
|
||||
*/
|
||||
|
||||
last_pause_ptr = via_align_cmd(dev_priv, HC_HAGPBpID_PAUSE, 0, &pause_addr_hi,
|
||||
&pause_addr_lo, 0) -1;
|
||||
via_align_cmd(dev_priv, HC_HAGPBpID_PAUSE, 0, &pause_addr_hi,
|
||||
&pause_addr_lo, 0);
|
||||
*last_pause_ptr = pause_addr_lo;
|
||||
|
||||
dma_low_save2 = dev_priv->dma_low;
|
||||
dev_priv->dma_low = dma_low_save1;
|
||||
via_hook_segment( dev_priv, jump_addr_hi, jump_addr_lo, 0);
|
||||
dev_priv->dma_low = dma_low_save2;
|
||||
via_hook_segment( dev_priv, pause_addr_hi, pause_addr_lo, 0);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -585,4 +640,64 @@ static void via_cmdbuf_reset(drm_via_private_t * dev_priv)
|
|||
via_wait_idle(dev_priv);
|
||||
}
|
||||
|
||||
/************************************************************************/
|
||||
/*
|
||||
* User interface to the space and lag function.
|
||||
*/
|
||||
|
||||
int
|
||||
via_cmdbuf_size(DRM_IOCTL_ARGS)
|
||||
{
|
||||
DRM_DEVICE;
|
||||
drm_via_cmdbuf_size_t d_siz;
|
||||
int ret = 0;
|
||||
uint32_t tmp_size, count;
|
||||
drm_via_private_t *dev_priv;
|
||||
|
||||
DRM_DEBUG("via cmdbuf_size\n");
|
||||
LOCK_TEST_WITH_RETURN( dev, filp );
|
||||
|
||||
dev_priv = (drm_via_private_t *) dev->dev_private;
|
||||
|
||||
if (dev_priv->ring.virtual_start == NULL) {
|
||||
DRM_ERROR("%s called without initializing AGP ring buffer.\n",
|
||||
__FUNCTION__);
|
||||
return DRM_ERR(EFAULT);
|
||||
}
|
||||
|
||||
DRM_COPY_FROM_USER_IOCTL(d_siz, (drm_via_cmdbuffer_t *) data,
|
||||
sizeof(d_siz));
|
||||
|
||||
|
||||
count = 1000000;
|
||||
tmp_size = d_siz.size;
|
||||
switch(d_siz.func) {
|
||||
case VIA_CMDBUF_SPACE:
|
||||
while (count-- && ((tmp_size = via_cmdbuf_space(dev_priv)) < d_siz.size)) {
|
||||
if (!d_siz.wait) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!count) {
|
||||
DRM_ERROR("VIA_CMDBUF_SPACE timed out.\n");
|
||||
ret = DRM_ERR(EAGAIN);
|
||||
}
|
||||
break;
|
||||
case VIA_CMDBUF_LAG:
|
||||
while (count-- && ((tmp_size = via_cmdbuf_lag(dev_priv)) > d_siz.size)) {
|
||||
if (!d_siz.wait) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!count) {
|
||||
DRM_ERROR("VIA_CMDBUF_SPACE timed out.\n");
|
||||
ret = DRM_ERR(EAGAIN);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return DRM_ERR(EFAULT);
|
||||
}
|
||||
d_siz.size = tmp_size;
|
||||
DRM_COPY_TO_USER_IOCTL((drm_via_cmdbuffer_t *) data, d_siz,
|
||||
sizeof(d_siz));
|
||||
return ret;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@
|
|||
#define DRM_IOCTL_VIA_CMDBUFFER DRM_IOW(0x48, drm_via_cmdbuffer_t)
|
||||
#define DRM_IOCTL_VIA_FLUSH DRM_IO(0x49)
|
||||
#define DRM_IOCTL_VIA_PCICMD DRM_IOW(0x4A, drm_via_cmdbuffer_t)
|
||||
#define DRM_IOCTL_VIA_CMDBUF_SIZE DRM_IOWR(0x4B, drm_via_cmdbuf_size_t)
|
||||
|
||||
|
||||
/* Indices into buf.Setup where various bits of state are mirrored per
|
||||
* context and per buffer. These can be fired at the card as a unit,
|
||||
|
|
@ -189,7 +191,14 @@ typedef struct _drm_via_flush_sys {
|
|||
int discard; /* client is finished with the buffer? */
|
||||
} drm_via_flush_sys_t;
|
||||
|
||||
|
||||
typedef struct _drm_via_cmdbuf_size {
|
||||
enum {
|
||||
VIA_CMDBUF_SPACE = 0x01,
|
||||
VIA_CMDBUF_LAG = 0x02
|
||||
} func;
|
||||
int wait;
|
||||
uint32_t size;
|
||||
} drm_via_cmdbuf_size_t;
|
||||
|
||||
#ifdef __KERNEL__
|
||||
|
||||
|
|
@ -203,6 +212,7 @@ int via_dma_init(DRM_IOCTL_ARGS);
|
|||
int via_cmdbuffer(DRM_IOCTL_ARGS);
|
||||
int via_flush_ioctl(DRM_IOCTL_ARGS);
|
||||
int via_pci_cmdbuffer(DRM_IOCTL_ARGS);
|
||||
int via_cmdbuf_size(DRM_IOCTL_ARGS);
|
||||
|
||||
#endif
|
||||
#endif /* _VIA_DRM_H_ */
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ typedef struct drm_via_private {
|
|||
unsigned int dma_low;
|
||||
unsigned int dma_high;
|
||||
unsigned int dma_offset;
|
||||
uint32_t dma_wrap;
|
||||
volatile uint32_t *last_pause_ptr;
|
||||
volatile uint32_t *hw_addr_ptr;
|
||||
drm_via_ring_buffer_t ring, fb_blit;
|
||||
|
|
|
|||
|
|
@ -254,39 +254,48 @@ typedef struct{
|
|||
sequence_t unfinished;
|
||||
int agp_texture;
|
||||
drm_device_t *dev;
|
||||
drm_map_t *map_cache;
|
||||
} sequence_context_t;
|
||||
|
||||
static sequence_context_t hc_sequence;
|
||||
|
||||
/*
|
||||
* stolen from drm_memory.h
|
||||
* Partially stolen from drm_memory.h
|
||||
*/
|
||||
|
||||
static __inline__ drm_map_t *
|
||||
via_drm_lookup_map (unsigned long offset, unsigned long size, drm_device_t *dev)
|
||||
via_drm_lookup_agp_map (sequence_context_t *seq, unsigned long offset, unsigned long size,
|
||||
drm_device_t *dev)
|
||||
{
|
||||
struct list_head *list;
|
||||
drm_map_list_t *r_list;
|
||||
drm_map_t *map;
|
||||
drm_map_t *map = seq->map_cache;
|
||||
|
||||
if (map && map->offset <= offset && (offset + size) <= (map->offset + map->size)) {
|
||||
return map;
|
||||
}
|
||||
|
||||
list_for_each(list, &dev->maplist->head) {
|
||||
r_list = (drm_map_list_t *) list;
|
||||
map = r_list->map;
|
||||
if (!map)
|
||||
continue;
|
||||
if (map->offset <= offset && (offset + size) <= (map->offset + map->size))
|
||||
if (map->offset <= offset && (offset + size) <= (map->offset + map->size) &&
|
||||
!(map->flags & _DRM_RESTRICTED) && (map->type == _DRM_AGP)) {
|
||||
seq->map_cache = map;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Require that all AGP texture levels reside in the same map which should
|
||||
* be mapped by the client. This is not a big restriction.
|
||||
* FIXME: To actually enforce this security policy strictly, drm_unmap
|
||||
* would have to wait for dma quiescent before unmapping an AGP page.
|
||||
* The via_drm_lookup_map call in reality seems to take
|
||||
* Require that all AGP texture levels reside in the same AGP map which should
|
||||
* be mappable by the client. This is not a big restriction.
|
||||
* FIXME: To actually enforce this security policy strictly, drm_rmmap
|
||||
* would have to wait for dma quiescent before removing an AGP map.
|
||||
* The via_drm_lookup_agp_map call in reality seems to take
|
||||
* very little CPU time.
|
||||
*/
|
||||
|
||||
|
|
@ -323,8 +332,8 @@ finish_current_sequence(sequence_context_t *cur_seq)
|
|||
if (tmp > hi) hi = tmp;
|
||||
}
|
||||
|
||||
if (! via_drm_lookup_map (lo, hi - lo, cur_seq->dev)) {
|
||||
DRM_ERROR("AGP texture is not in client map\n");
|
||||
if (! via_drm_lookup_agp_map (cur_seq, lo, hi - lo, cur_seq->dev)) {
|
||||
DRM_ERROR("AGP texture is not in allowed map\n");
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
|
@ -620,6 +629,7 @@ via_verify_command_stream(const uint32_t * buf, unsigned int size, drm_device_t
|
|||
|
||||
hc_sequence.dev = dev;
|
||||
hc_sequence.unfinished = no_sequence;
|
||||
hc_sequence.map_cache = NULL;
|
||||
|
||||
while (buf < buf_end) {
|
||||
switch (state) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue