561 lines
18 KiB
C
561 lines
18 KiB
C
// sis.c -- SiS 6326/300/305/315/330 accelerated video driver
|
|
//
|
|
// Supports the SiS 6326, 300, 305, 315, and 330 integrated graphics
|
|
// chipsets. These share a similar 2D engine interface based on a
|
|
// queue-based command submission model:
|
|
// - Hardware rectangle fill
|
|
// - Screen-to-screen BitBLT
|
|
// - CPU-to-screen blit (host blit via data port)
|
|
// - Hardware clip rectangle
|
|
// - 64x64 hardware cursor
|
|
//
|
|
// Register access:
|
|
// BAR0 maps the linear framebuffer.
|
|
// BAR1 maps 128KB of MMIO registers. The 2D engine registers
|
|
// live at offsets 0x8200-0x8244 within this block. Host data
|
|
// is written to the MMIO data port at offset 0x8300.
|
|
//
|
|
// The 2D engine uses a command register at 0x822C to specify the
|
|
// operation type and ROP, then a fire register at 0x8230 to trigger
|
|
// execution. Engine status is polled at 0x8244.
|
|
|
|
#include "accelVid.h"
|
|
#include "vgaCommon.h"
|
|
#include "pci.h"
|
|
|
|
#include <pc.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
// ============================================================
|
|
// SiS vendor/device IDs
|
|
// ============================================================
|
|
|
|
#define SIS_VENDOR_ID 0x1039
|
|
|
|
#define SIS_6326 0x6326
|
|
#define SIS_300 0x0300
|
|
#define SIS_305 0x0305
|
|
#define SIS_315 0x0315
|
|
#define SIS_330 0x0330
|
|
|
|
static const uint16_t sSisDeviceIds[] = {
|
|
SIS_VENDOR_ID, SIS_6326,
|
|
SIS_VENDOR_ID, SIS_300,
|
|
SIS_VENDOR_ID, SIS_305,
|
|
SIS_VENDOR_ID, SIS_315,
|
|
SIS_VENDOR_ID, SIS_330,
|
|
0, 0
|
|
};
|
|
|
|
// ============================================================
|
|
// 2D engine register offsets (from MMIO base)
|
|
// ============================================================
|
|
|
|
#define SIS_SRC_ADDR 0x8200 // source address (for blit)
|
|
#define SIS_SRC_PITCH 0x8204 // source pitch
|
|
#define SIS_SRC_YX 0x8208 // src Y<<16 | X
|
|
#define SIS_DST_YX 0x820C // dst Y<<16 | X
|
|
#define SIS_RECT_WH 0x8210 // width<<16 | height
|
|
#define SIS_FG_COLOR 0x8214 // foreground color
|
|
#define SIS_BG_COLOR 0x8218 // background color
|
|
#define SIS_MONO_PAT0 0x821C // mono pattern 0
|
|
#define SIS_MONO_PAT1 0x8220 // mono pattern 1
|
|
#define SIS_CLIP_LT 0x8224 // clip left<<16 | top
|
|
#define SIS_CLIP_RB 0x8228 // clip right<<16 | bottom
|
|
#define SIS_CMD 0x822C // command register
|
|
#define SIS_FIRE 0x8230 // fire trigger
|
|
#define SIS_LINE_PARAMS 0x8234 // line parameters
|
|
#define SIS_DST_ADDR 0x8238 // destination address
|
|
#define SIS_SRC_DST_PITCH 0x823C // src/dst pitch combined
|
|
#define SIS_AGP_BASE 0x8240 // AGP base (unused)
|
|
|
|
// ============================================================
|
|
// Engine status register
|
|
// ============================================================
|
|
|
|
#define SIS_ENGINE_STATUS 0x8244 // bit 0 = queues empty, bit 1 = idle
|
|
|
|
#define SIS_STATUS_QUEUE_EMPTY 0x01
|
|
#define SIS_STATUS_ENGINE_IDLE 0x02
|
|
#define SIS_STATUS_ALL_IDLE (SIS_STATUS_QUEUE_EMPTY | SIS_STATUS_ENGINE_IDLE)
|
|
|
|
// ============================================================
|
|
// Host data port
|
|
// ============================================================
|
|
|
|
#define SIS_HOST_DATA 0x8300 // write pixel data here as dwords
|
|
|
|
// ============================================================
|
|
// Command register encoding
|
|
// ============================================================
|
|
|
|
// Bits 7:0 = ROP
|
|
#define SIS_ROP_COPY 0xCC
|
|
#define SIS_ROP_PAT_COPY 0xF0
|
|
|
|
// Bit 8 = X direction
|
|
#define SIS_CMD_XDIR_RIGHT (1 << 8)
|
|
|
|
// Bit 9 = Y direction
|
|
#define SIS_CMD_YDIR_DOWN (1 << 9)
|
|
|
|
// Bits 13:10 = command type
|
|
#define SIS_CMD_BITBLT 0x0000
|
|
#define SIS_CMD_COLOREXP 0x0400
|
|
#define SIS_CMD_LINEDRAW 0x0800
|
|
#define SIS_CMD_TRAPEZOID 0x0C00
|
|
|
|
// Bit 14 = pattern enable
|
|
#define SIS_CMD_PAT_ENABLE (1 << 14)
|
|
|
|
// Bit 16 = clipping enable
|
|
#define SIS_CMD_CLIP_ENABLE (1 << 16)
|
|
|
|
// Bit 24 = source is mono
|
|
#define SIS_CMD_SRC_MONO (1 << 24)
|
|
|
|
// ============================================================
|
|
// Hardware cursor registers
|
|
// ============================================================
|
|
|
|
#define SIS_CURSOR_ENABLE 0x8500 // bit 0 = enable
|
|
#define SIS_CURSOR_X 0x8504 // cursor X position
|
|
#define SIS_CURSOR_Y 0x8508 // cursor Y position
|
|
#define SIS_CURSOR_ADDR 0x850C // cursor VRAM byte offset
|
|
|
|
// ============================================================
|
|
// Misc constants
|
|
// ============================================================
|
|
|
|
#define SIS_MMIO_SIZE 131072 // BAR1: 128KB MMIO
|
|
#define SIS_MAX_IDLE_WAIT 1000000
|
|
#define SIS_HW_CURSOR_SIZE 64
|
|
|
|
// ============================================================
|
|
// Private driver state
|
|
// ============================================================
|
|
|
|
typedef struct {
|
|
uint32_t lfbPhysAddr;
|
|
uint32_t mmioPhysAddr;
|
|
uint32_t vramSize;
|
|
int32_t bytesPerPixel;
|
|
int32_t screenPitch;
|
|
volatile uint32_t *mmio;
|
|
DpmiMappingT mmioMapping;
|
|
DpmiMappingT lfbMapping;
|
|
} SisPrivateT;
|
|
|
|
// ============================================================
|
|
// Prototypes
|
|
// ============================================================
|
|
|
|
static void sisBitBlt(AccelDriverT *drv, int32_t srcX, int32_t srcY, int32_t dstX, int32_t dstY, int32_t w, int32_t h);
|
|
static bool sisDetect(AccelDriverT *drv);
|
|
static void sisHostBlit(AccelDriverT *drv, const uint8_t *srcBuf, int32_t srcPitch, int32_t dstX, int32_t dstY, int32_t w, int32_t h);
|
|
static bool sisInit(AccelDriverT *drv, const AccelModeRequestT *req);
|
|
static void sisMoveCursor(AccelDriverT *drv, int32_t x, int32_t y);
|
|
static void sisRectFill(AccelDriverT *drv, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color);
|
|
static void sisSetClip(AccelDriverT *drv, int32_t x, int32_t y, int32_t w, int32_t h);
|
|
static void sisSetCursor(AccelDriverT *drv, const HwCursorImageT *image);
|
|
static void sisShowCursor(AccelDriverT *drv, bool visible);
|
|
static void sisShutdown(AccelDriverT *drv);
|
|
static void sisWaitIdle(AccelDriverT *drv);
|
|
|
|
static inline void sisWrite(SisPrivateT *priv, uint32_t reg, uint32_t val) {
|
|
priv->mmio[reg / 4] = val;
|
|
}
|
|
|
|
static inline uint32_t sisRead(SisPrivateT *priv, uint32_t reg) {
|
|
return priv->mmio[reg / 4];
|
|
}
|
|
|
|
// ============================================================
|
|
// Driver instance
|
|
// ============================================================
|
|
|
|
static SisPrivateT sSisPrivate;
|
|
|
|
static AccelDriverT sSisDriver = {
|
|
.name = "SiS 6326",
|
|
.chipFamily = "sis",
|
|
.caps = 0,
|
|
.privData = &sSisPrivate,
|
|
.detect = sisDetect,
|
|
.init = sisInit,
|
|
.shutdown = sisShutdown,
|
|
.waitIdle = sisWaitIdle,
|
|
.setClip = sisSetClip,
|
|
.rectFill = sisRectFill,
|
|
.rectFillPat = NULL,
|
|
.bitBlt = sisBitBlt,
|
|
.hostBlit = sisHostBlit,
|
|
.colorExpand = NULL,
|
|
.lineDraw = NULL,
|
|
.setCursor = sisSetCursor,
|
|
.moveCursor = sisMoveCursor,
|
|
.showCursor = sisShowCursor,
|
|
};
|
|
|
|
// ============================================================
|
|
// sisRegisterDriver
|
|
// ============================================================
|
|
|
|
void sisRegisterDriver(void) {
|
|
accelRegisterDriver(&sSisDriver);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// sisBitBlt
|
|
// ============================================================
|
|
//
|
|
// Screen-to-screen BitBLT. Handles overlapping regions by choosing
|
|
// the correct X/Y direction based on source and destination positions.
|
|
|
|
static void sisBitBlt(AccelDriverT *drv, int32_t srcX, int32_t srcY, int32_t dstX, int32_t dstY, int32_t w, int32_t h) {
|
|
SisPrivateT *priv = (SisPrivateT *)drv->privData;
|
|
|
|
if (w <= 0 || h <= 0) {
|
|
return;
|
|
}
|
|
|
|
sisWaitIdle(drv);
|
|
|
|
// Determine blit direction for overlapping regions
|
|
uint32_t cmd = SIS_CMD_BITBLT | SIS_ROP_COPY | SIS_CMD_CLIP_ENABLE;
|
|
int32_t sx = srcX;
|
|
int32_t sy = srcY;
|
|
int32_t dx = dstX;
|
|
int32_t dy = dstY;
|
|
|
|
if (dstX <= srcX) {
|
|
cmd |= SIS_CMD_XDIR_RIGHT;
|
|
} else {
|
|
sx += w - 1;
|
|
dx += w - 1;
|
|
}
|
|
|
|
if (dstY <= srcY) {
|
|
cmd |= SIS_CMD_YDIR_DOWN;
|
|
} else {
|
|
sy += h - 1;
|
|
dy += h - 1;
|
|
}
|
|
|
|
uint32_t pitch = ((uint32_t)priv->screenPitch << 16) | (uint32_t)priv->screenPitch;
|
|
|
|
sisWrite(priv, SIS_SRC_DST_PITCH, pitch);
|
|
sisWrite(priv, SIS_SRC_YX, ((uint32_t)sy << 16) | (uint32_t)sx);
|
|
sisWrite(priv, SIS_DST_YX, ((uint32_t)dy << 16) | (uint32_t)dx);
|
|
sisWrite(priv, SIS_RECT_WH, ((uint32_t)w << 16) | (uint32_t)h);
|
|
sisWrite(priv, SIS_CMD, cmd);
|
|
sisWrite(priv, SIS_FIRE, 0);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// sisDetect
|
|
// ============================================================
|
|
|
|
static bool sisDetect(AccelDriverT *drv) {
|
|
int32_t matchIdx;
|
|
|
|
if (!pciFindDeviceList(sSisDeviceIds, &drv->pciDev, &matchIdx)) {
|
|
return false;
|
|
}
|
|
|
|
switch (drv->pciDev.deviceId) {
|
|
case SIS_6326:
|
|
drv->name = "SiS 6326";
|
|
break;
|
|
case SIS_300:
|
|
drv->name = "SiS 300";
|
|
break;
|
|
case SIS_305:
|
|
drv->name = "SiS 305";
|
|
break;
|
|
case SIS_315:
|
|
drv->name = "SiS 315";
|
|
break;
|
|
case SIS_330:
|
|
drv->name = "SiS 330";
|
|
break;
|
|
default:
|
|
drv->name = "SiS 6326/3xx";
|
|
break;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// sisHostBlit
|
|
// ============================================================
|
|
//
|
|
// CPU-to-screen blit. Issues a BitBLT command, then feeds pixel data
|
|
// as dwords through the MMIO host data port at offset 0x8300.
|
|
|
|
static void sisHostBlit(AccelDriverT *drv, const uint8_t *srcBuf, int32_t srcPitch, int32_t dstX, int32_t dstY, int32_t w, int32_t h) {
|
|
SisPrivateT *priv = (SisPrivateT *)drv->privData;
|
|
|
|
if (w <= 0 || h <= 0) {
|
|
return;
|
|
}
|
|
|
|
int32_t bytesPerRow = w * priv->bytesPerPixel;
|
|
int32_t dwordsPerRow = (bytesPerRow + 3) / 4;
|
|
|
|
sisWaitIdle(drv);
|
|
|
|
sisWrite(priv, SIS_SRC_DST_PITCH, (uint32_t)priv->screenPitch);
|
|
sisWrite(priv, SIS_DST_YX, ((uint32_t)dstY << 16) | (uint32_t)dstX);
|
|
sisWrite(priv, SIS_RECT_WH, ((uint32_t)w << 16) | (uint32_t)h);
|
|
sisWrite(priv, SIS_FG_COLOR, 0);
|
|
sisWrite(priv, SIS_CMD, SIS_CMD_BITBLT | SIS_ROP_COPY | SIS_CMD_CLIP_ENABLE | SIS_CMD_XDIR_RIGHT | SIS_CMD_YDIR_DOWN | SIS_CMD_SRC_MONO);
|
|
sisWrite(priv, SIS_FIRE, 0);
|
|
|
|
// Feed pixel data row by row through the host data port
|
|
for (int32_t row = 0; row < h; row++) {
|
|
const uint8_t *rowPtr = srcBuf + row * srcPitch;
|
|
|
|
for (int32_t dw = 0; dw < dwordsPerRow; dw++) {
|
|
uint32_t val = 0;
|
|
int32_t offset = dw * 4;
|
|
|
|
for (int32_t b = 0; b < 4; b++) {
|
|
if (offset + b < bytesPerRow) {
|
|
val |= (uint32_t)rowPtr[offset + b] << (b * 8);
|
|
}
|
|
}
|
|
|
|
sisWrite(priv, SIS_HOST_DATA, val);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// sisInit
|
|
// ============================================================
|
|
|
|
static bool sisInit(AccelDriverT *drv, const AccelModeRequestT *req) {
|
|
SisPrivateT *priv = (SisPrivateT *)drv->privData;
|
|
|
|
// Read BARs
|
|
uint32_t bar0 = pciRead32(drv->pciDev.bus, drv->pciDev.dev,
|
|
drv->pciDev.func, PCI_BAR0);
|
|
uint32_t bar1 = pciRead32(drv->pciDev.bus, drv->pciDev.dev,
|
|
drv->pciDev.func, PCI_BAR1);
|
|
|
|
priv->lfbPhysAddr = bar0 & 0xFFFFFFF0;
|
|
priv->mmioPhysAddr = bar1 & 0xFFFFFFF0;
|
|
|
|
// Size the framebuffer BAR
|
|
priv->vramSize = pciSizeBar(drv->pciDev.bus, drv->pciDev.dev,
|
|
drv->pciDev.func, PCI_BAR0);
|
|
|
|
// Map MMIO control registers (128KB)
|
|
if (!dpmiMapFramebuffer(priv->mmioPhysAddr, SIS_MMIO_SIZE, &priv->mmioMapping)) {
|
|
return false;
|
|
}
|
|
priv->mmio = (volatile uint32_t *)priv->mmioMapping.ptr;
|
|
|
|
// Find and set VESA mode
|
|
VesaModeResultT vesa;
|
|
if (!vesaFindAndSetMode(req->width, req->height, req->bpp, &vesa)) {
|
|
dpmiUnmapFramebuffer(&priv->mmioMapping);
|
|
return false;
|
|
}
|
|
|
|
// Map framebuffer
|
|
if (!dpmiMapFramebuffer(priv->lfbPhysAddr, priv->vramSize, &priv->lfbMapping)) {
|
|
vgaRestoreTextMode();
|
|
dpmiUnmapFramebuffer(&priv->mmioMapping);
|
|
return false;
|
|
}
|
|
|
|
priv->bytesPerPixel = (vesa.bpp + 7) / 8;
|
|
priv->screenPitch = vesa.pitch;
|
|
|
|
drv->mode.width = vesa.width;
|
|
drv->mode.height = vesa.height;
|
|
drv->mode.bpp = vesa.bpp;
|
|
drv->mode.pitch = vesa.pitch;
|
|
drv->mode.framebuffer = priv->lfbMapping.ptr;
|
|
drv->mode.vramSize = priv->vramSize;
|
|
drv->mode.offscreenBase = vesa.pitch * vesa.height;
|
|
|
|
// Wait for engine idle before configuring
|
|
sisWaitIdle(drv);
|
|
|
|
drv->caps = ACAP_RECT_FILL
|
|
| ACAP_BITBLT
|
|
| ACAP_HOST_BLIT
|
|
| ACAP_HW_CURSOR
|
|
| ACAP_CLIP;
|
|
|
|
// Full screen clip
|
|
sisSetClip(drv, 0, 0, vesa.width, vesa.height);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// sisMoveCursor
|
|
// ============================================================
|
|
|
|
static void sisMoveCursor(AccelDriverT *drv, int32_t x, int32_t y) {
|
|
SisPrivateT *priv = (SisPrivateT *)drv->privData;
|
|
|
|
if (x < 0) {
|
|
x = 0;
|
|
}
|
|
if (y < 0) {
|
|
y = 0;
|
|
}
|
|
|
|
sisWrite(priv, SIS_CURSOR_X, (uint32_t)x);
|
|
sisWrite(priv, SIS_CURSOR_Y, (uint32_t)y);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// sisRectFill
|
|
// ============================================================
|
|
//
|
|
// Solid rectangle fill. Sets the foreground color, loads the
|
|
// destination coordinates and dimensions, then fires a BitBLT
|
|
// command with PAT_COPY ROP and pattern enable to fill with a
|
|
// solid color.
|
|
|
|
static void sisRectFill(AccelDriverT *drv, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color) {
|
|
SisPrivateT *priv = (SisPrivateT *)drv->privData;
|
|
|
|
if (w <= 0 || h <= 0) {
|
|
return;
|
|
}
|
|
|
|
sisWaitIdle(drv);
|
|
|
|
sisWrite(priv, SIS_SRC_DST_PITCH, (uint32_t)priv->screenPitch);
|
|
sisWrite(priv, SIS_FG_COLOR, color);
|
|
sisWrite(priv, SIS_MONO_PAT0, 0xFFFFFFFF);
|
|
sisWrite(priv, SIS_MONO_PAT1, 0xFFFFFFFF);
|
|
sisWrite(priv, SIS_DST_YX, ((uint32_t)y << 16) | (uint32_t)x);
|
|
sisWrite(priv, SIS_RECT_WH, ((uint32_t)w << 16) | (uint32_t)h);
|
|
sisWrite(priv, SIS_CMD, SIS_CMD_BITBLT | SIS_ROP_PAT_COPY | SIS_CMD_PAT_ENABLE | SIS_CMD_CLIP_ENABLE | SIS_CMD_XDIR_RIGHT | SIS_CMD_YDIR_DOWN);
|
|
sisWrite(priv, SIS_FIRE, 0);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// sisSetClip
|
|
// ============================================================
|
|
|
|
static void sisSetClip(AccelDriverT *drv, int32_t x, int32_t y, int32_t w, int32_t h) {
|
|
SisPrivateT *priv = (SisPrivateT *)drv->privData;
|
|
|
|
sisWrite(priv, SIS_CLIP_LT, ((uint32_t)x << 16) | (uint32_t)y);
|
|
sisWrite(priv, SIS_CLIP_RB, ((uint32_t)(x + w - 1) << 16) | (uint32_t)(y + h - 1));
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// sisSetCursor
|
|
// ============================================================
|
|
//
|
|
// Upload a 64x64 hardware cursor image to VRAM. The SiS cursor
|
|
// format is 2bpp: AND mask and XOR mask interleaved per row,
|
|
// 16 bytes per row (8 AND + 8 XOR). Total size is 1024 bytes.
|
|
|
|
static void sisSetCursor(AccelDriverT *drv, const HwCursorImageT *image) {
|
|
SisPrivateT *priv = (SisPrivateT *)drv->privData;
|
|
|
|
if (!image) {
|
|
sisShowCursor(drv, false);
|
|
return;
|
|
}
|
|
|
|
sisWaitIdle(drv);
|
|
|
|
// Store cursor image at end of VRAM (1KB aligned)
|
|
uint32_t cursorOffset = priv->vramSize - 1024;
|
|
cursorOffset &= ~0x3FF;
|
|
uint8_t *cursorMem = drv->mode.framebuffer + cursorOffset;
|
|
|
|
// Write AND mask then XOR mask, interleaved per row
|
|
for (int32_t row = 0; row < SIS_HW_CURSOR_SIZE; row++) {
|
|
for (int32_t byteIdx = 0; byteIdx < 8; byteIdx++) {
|
|
int32_t srcIdx = row * 8 + byteIdx;
|
|
uint8_t andByte;
|
|
uint8_t xorByte;
|
|
|
|
if (row < image->height && byteIdx < (image->width + 7) / 8) {
|
|
andByte = image->andMask[srcIdx];
|
|
xorByte = image->xorMask[srcIdx];
|
|
} else {
|
|
andByte = 0xFF; // transparent
|
|
xorByte = 0x00;
|
|
}
|
|
|
|
cursorMem[row * 16 + byteIdx] = andByte;
|
|
cursorMem[row * 16 + byteIdx + 8] = xorByte;
|
|
}
|
|
}
|
|
|
|
// Set cursor address register
|
|
sisWrite(priv, SIS_CURSOR_ADDR, cursorOffset);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// sisShowCursor
|
|
// ============================================================
|
|
|
|
static void sisShowCursor(AccelDriverT *drv, bool visible) {
|
|
SisPrivateT *priv = (SisPrivateT *)drv->privData;
|
|
|
|
sisWrite(priv, SIS_CURSOR_ENABLE, visible ? 1 : 0);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// sisShutdown
|
|
// ============================================================
|
|
|
|
static void sisShutdown(AccelDriverT *drv) {
|
|
SisPrivateT *priv = (SisPrivateT *)drv->privData;
|
|
|
|
sisShowCursor(drv, false);
|
|
vgaRestoreTextMode();
|
|
|
|
dpmiUnmapFramebuffer(&priv->lfbMapping);
|
|
dpmiUnmapFramebuffer(&priv->mmioMapping);
|
|
|
|
priv->mmio = NULL;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// sisWaitIdle
|
|
// ============================================================
|
|
//
|
|
// Wait until the 2D engine is completely idle. Both bit 0 (queues
|
|
// empty) and bit 1 (engine idle) of the status register at 0x8244
|
|
// must be set.
|
|
|
|
static void sisWaitIdle(AccelDriverT *drv) {
|
|
SisPrivateT *priv = (SisPrivateT *)drv->privData;
|
|
|
|
for (int32_t i = 0; i < SIS_MAX_IDLE_WAIT; i++) {
|
|
uint32_t stat = sisRead(priv, SIS_ENGINE_STATUS);
|
|
if ((stat & SIS_STATUS_ALL_IDLE) == SIS_STATUS_ALL_IDLE) {
|
|
return;
|
|
}
|
|
}
|
|
}
|