fixed simX multicore support, added shared memory
This commit is contained in:
378
simX/mem.cpp
378
simX/mem.cpp
@@ -3,7 +3,9 @@
|
||||
#include <iomanip>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <stdlib.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include "debug.h"
|
||||
#include "types.h"
|
||||
@@ -13,8 +15,8 @@
|
||||
|
||||
using namespace vortex;
|
||||
|
||||
RamMemDevice::RamMemDevice(const char *filename, Size wordSize)
|
||||
: wordSize(wordSize), contents() {
|
||||
RamMemDevice::RamMemDevice(const char *filename, Size wordSize)
|
||||
: wordSize_(wordSize) {
|
||||
std::ifstream input(filename);
|
||||
|
||||
if (!input) {
|
||||
@@ -23,15 +25,17 @@ RamMemDevice::RamMemDevice(const char *filename, Size wordSize)
|
||||
}
|
||||
|
||||
do {
|
||||
contents.push_back(input.get());
|
||||
contents_.push_back(input.get());
|
||||
} while (input);
|
||||
|
||||
while (contents.size() % wordSize)
|
||||
contents.push_back(0x00);
|
||||
while (contents_.size() % wordSize)
|
||||
contents_.push_back(0x00);
|
||||
}
|
||||
|
||||
RamMemDevice::RamMemDevice(Size size, Size wordSize)
|
||||
: wordSize(wordSize), contents(size) {}
|
||||
: wordSize_(wordSize)
|
||||
, contents_(size)
|
||||
{}
|
||||
|
||||
void RomMemDevice::write(Addr, Word) {
|
||||
std::cout << "Attempt to write to ROM.\n";
|
||||
@@ -40,87 +44,81 @@ void RomMemDevice::write(Addr, Word) {
|
||||
|
||||
Word RamMemDevice::read(Addr addr) {
|
||||
D(2, "RAM read, addr=0x" << std::hex << addr);
|
||||
Word w = readWord(contents, addr, wordSize - addr % wordSize);
|
||||
Word w = readWord(contents_, addr, wordSize_ - addr % wordSize_);
|
||||
return w;
|
||||
}
|
||||
|
||||
void RamMemDevice::write(Addr addr, Word w) {
|
||||
D(2, "RAM write, addr=0x" << std::hex << addr);
|
||||
writeWord(contents, addr, wordSize - addr % wordSize, w);
|
||||
writeWord(contents_, addr, wordSize_ - addr % wordSize_, w);
|
||||
}
|
||||
|
||||
MemDevice &MemoryUnit::ADecoder::doLookup(Addr a, Size &bit) {
|
||||
if (range == 0 || (a & ((1ll << bit) - 1)) >= range) {
|
||||
ADecoder *p(((a >> bit) & 1) ? oneChild : zeroChild);
|
||||
if (p) {
|
||||
bit--;
|
||||
return p->doLookup(a, bit);
|
||||
} else {
|
||||
std::cout << "lookup of 0x" << std::hex << a << " failed.\n";
|
||||
throw BadAddress();
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
bool MemoryUnit::ADecoder::lookup(Addr a, Size wordSize, mem_accessor_t* ma) {
|
||||
Addr e = a + (wordSize - 1);
|
||||
assert(e >= a);
|
||||
for (auto iter = entries_.rbegin(), iterE = entries_.rend(); iter != iterE; ++iter) {
|
||||
if (a >= iter->start && e <= iter->end) {
|
||||
ma->md = iter->md;
|
||||
ma->addr = a - iter->start;
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return *md;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void MemoryUnit::ADecoder::map(Addr a, MemDevice &m, Size r, Size bit) {
|
||||
if ((1llu << bit) <= r) {
|
||||
md = &m;
|
||||
range = m.size();
|
||||
} else {
|
||||
ADecoder *&child(((a >> bit) & 1) ? oneChild : zeroChild);
|
||||
if (!child)
|
||||
child = new ADecoder();
|
||||
child->map(a, m, r, bit - 1);
|
||||
}
|
||||
}
|
||||
|
||||
Byte *MemoryUnit::ADecoder::getPtr(Addr a, Size sz, Size wordSize) {
|
||||
Size bit = wordSize - 1;
|
||||
MemDevice &m(doLookup(a, bit));
|
||||
a &= (2 << bit) - 1;
|
||||
if (a + sz <= m.size())
|
||||
return m.base() + a;
|
||||
return NULL;
|
||||
void MemoryUnit::ADecoder::map(Addr a, Addr e, MemDevice &m) {
|
||||
assert(e >= a);
|
||||
entry_t entry{&m, a, e};
|
||||
entries_.emplace_back(entry);
|
||||
}
|
||||
|
||||
Word MemoryUnit::ADecoder::read(Addr a, bool /*sup*/, Size wordSize) {
|
||||
Size bit = wordSize - 1;
|
||||
MemDevice &m(doLookup(a, bit));
|
||||
a &= (2 << bit) - 1;
|
||||
return m.read(a);
|
||||
mem_accessor_t ma;
|
||||
if (!this->lookup(a, wordSize, &ma)) {
|
||||
std::cout << "lookup of 0x" << std::hex << a << " failed.\n";
|
||||
throw BadAddress();
|
||||
}
|
||||
return ma.md->read(ma.addr);
|
||||
}
|
||||
|
||||
void MemoryUnit::ADecoder::write(Addr a, Word w, bool /*sup*/, Size wordSize) {
|
||||
Size bit = wordSize - 1;
|
||||
MemDevice &m(doLookup(a, bit));
|
||||
|
||||
RAM &r = (RAM &)m;
|
||||
|
||||
mem_accessor_t ma;
|
||||
if (!this->lookup(a, wordSize, &ma)) {
|
||||
std::cout << "lookup of 0x" << std::hex << a << " failed.\n";
|
||||
throw BadAddress();
|
||||
}
|
||||
RAM *ram = (RAM *)ma.md;
|
||||
if (wordSize == 8) {
|
||||
r.writeByte(a, &w);
|
||||
ram->writeByte(ma.addr, &w);
|
||||
} else if (wordSize == 16) {
|
||||
r.writeHalf(a, &w);
|
||||
ram->writeHalf(ma.addr, &w);
|
||||
} else {
|
||||
r.writeWord(a, &w);
|
||||
ram->writeWord(ma.addr, &w);
|
||||
}
|
||||
}
|
||||
|
||||
Byte *MemoryUnit::getPtr(Addr a, Size s) {
|
||||
return ad.getPtr(a, s, addrBytes * 8);
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
MemoryUnit::MemoryUnit(Size pageSize, Size addrBytes, bool disableVm)
|
||||
: pageSize_(pageSize)
|
||||
, addrBytes_(addrBytes)
|
||||
, disableVm_(disableVm) {
|
||||
if (!disableVm) {
|
||||
tlb_[0] = TLBEntry(0, 077);
|
||||
}
|
||||
}
|
||||
|
||||
void MemoryUnit::attach(MemDevice &m, Addr base) {
|
||||
ad.map(base, m, m.size(), addrBytes * 8 - 1);
|
||||
void MemoryUnit::attach(MemDevice &m, Addr start, Addr end) {
|
||||
decoder_.map(start, end, m);
|
||||
}
|
||||
|
||||
MemoryUnit::TLBEntry MemoryUnit::tlbLookup(Addr vAddr, Word flagMask) {
|
||||
std::unordered_map<Addr, MemoryUnit::TLBEntry>::iterator i;
|
||||
if ((i = tlb.find(vAddr / pageSize)) != tlb.end()) {
|
||||
TLBEntry &t = i->second;
|
||||
if (t.flags & flagMask)
|
||||
return t;
|
||||
auto iter = tlb_.find(vAddr / pageSize_);
|
||||
if (iter != tlb_.end()) {
|
||||
if (iter->second.flags & flagMask)
|
||||
return iter->second;
|
||||
else {
|
||||
D(2, "Page fault on addr 0x" << std::hex << vAddr << "(bad flags)");
|
||||
throw PageFault(vAddr, false);
|
||||
@@ -133,86 +131,81 @@ MemoryUnit::TLBEntry MemoryUnit::tlbLookup(Addr vAddr, Word flagMask) {
|
||||
|
||||
Word MemoryUnit::read(Addr vAddr, bool sup) {
|
||||
Addr pAddr;
|
||||
if (disableVm) {
|
||||
if (disableVm_) {
|
||||
pAddr = vAddr;
|
||||
} else {
|
||||
Word flagMask = sup ? 8 : 1;
|
||||
TLBEntry t = tlbLookup(vAddr, flagMask);
|
||||
pAddr = t.pfn * pageSize + vAddr % pageSize;
|
||||
TLBEntry t = this->tlbLookup(vAddr, flagMask);
|
||||
pAddr = t.pfn * pageSize_ + vAddr % pageSize_;
|
||||
}
|
||||
// std::cout << "MU::write: About to read: " << std::hex << pAddr << " = " << (ad.read(pAddr, sup, 8*addrBytes)) << " with " << std::dec << (8*addrBytes) << "\n";
|
||||
return ad.read(pAddr, sup, 8 * addrBytes);
|
||||
return decoder_.read(pAddr, sup, addrBytes_);
|
||||
}
|
||||
|
||||
Word MemoryUnit::fetch(Addr vAddr, bool sup) {
|
||||
Addr pAddr;
|
||||
|
||||
if (disableVm) {
|
||||
if (disableVm_) {
|
||||
pAddr = vAddr;
|
||||
} else {
|
||||
Word flagMask = sup ? 32 : 4;
|
||||
TLBEntry t = tlbLookup(vAddr, flagMask);
|
||||
pAddr = t.pfn * pageSize + vAddr % pageSize;
|
||||
TLBEntry t = this->tlbLookup(vAddr, flagMask);
|
||||
pAddr = t.pfn * pageSize_ + vAddr % pageSize_;
|
||||
}
|
||||
|
||||
Word instruction = ad.read(pAddr, sup, 8 * addrBytes);
|
||||
|
||||
Word instruction = decoder_.read(pAddr, sup, addrBytes_);
|
||||
return instruction;
|
||||
}
|
||||
|
||||
void MemoryUnit::write(Addr vAddr, Word w, bool sup, Size bytes) {
|
||||
Addr pAddr;
|
||||
|
||||
if (disableVm) {
|
||||
if (disableVm_) {
|
||||
pAddr = vAddr;
|
||||
} else {
|
||||
Word flagMask = sup ? 16 : 2;
|
||||
TLBEntry t = tlbLookup(vAddr, flagMask);
|
||||
pAddr = t.pfn * pageSize + vAddr % pageSize;
|
||||
pAddr = t.pfn * pageSize_ + vAddr % pageSize_;
|
||||
}
|
||||
// std::cout << "MU::write: About to write: " << std::hex << pAddr << " = " << w << " with " << std::dec << 8*bytes << "\n";
|
||||
ad.write(pAddr, w, sup, 8 * bytes);
|
||||
// std::cout << std::hex << "reading same address: " << (this->read(vAddr, sup)) << "\n";
|
||||
decoder_.write(pAddr, w, sup, bytes);
|
||||
}
|
||||
|
||||
void MemoryUnit::tlbAdd(Addr virt, Addr phys, Word flags) {
|
||||
D(1, "tlbAdd(0x" << std::hex << virt << ", 0x" << phys << ", 0x" << flags << ')');
|
||||
tlb[virt / pageSize] = TLBEntry(phys / pageSize, flags);
|
||||
tlb_[virt / pageSize_] = TLBEntry(phys / pageSize_, flags);
|
||||
}
|
||||
|
||||
void MemoryUnit::tlbRm(Addr va) {
|
||||
if (tlb.find(va / pageSize) != tlb.end())
|
||||
tlb.erase(tlb.find(va / pageSize));
|
||||
if (tlb_.find(va / pageSize_) != tlb_.end())
|
||||
tlb_.erase(tlb_.find(va / pageSize_));
|
||||
}
|
||||
|
||||
void *vortex::consoleInputThread(void */*arg_vp*/) {
|
||||
// ConsoleMemDevice *arg = (ConsoleMemDevice *)arg_vp;
|
||||
// char c;
|
||||
// while (cin) {
|
||||
// c = cin.get();
|
||||
// pthread_mutex_lock(&arg->cBufLock);
|
||||
// arg->cBuf.push(c);
|
||||
// pthread_mutex_unlock(&arg->cBufLock);
|
||||
// }
|
||||
// cout << "Console input ended. Exiting.\n";
|
||||
// exit(4);
|
||||
void *vortex::consoleInputThread(void * /*arg_vp*/) {
|
||||
//--
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
DiskControllerMemDevice::DiskControllerMemDevice(Size wordSize, Size blockSize, Core &c)
|
||||
: wordSize_(wordSize)
|
||||
, blockSize_(blockSize)
|
||||
, core_(c)
|
||||
{}
|
||||
|
||||
Word DiskControllerMemDevice::read(Addr a) {
|
||||
switch (a / 8) {
|
||||
case 0:
|
||||
return curDisk;
|
||||
return curDisk_;
|
||||
case 1:
|
||||
return curBlock;
|
||||
return curBlock_;
|
||||
case 2:
|
||||
return disks[curDisk].blocks * blockSize;
|
||||
return disks_[curDisk_].blocks * blockSize_;
|
||||
case 3:
|
||||
return physAddr;
|
||||
return physAddr_;
|
||||
case 4:
|
||||
return command;
|
||||
return command_;
|
||||
case 5:
|
||||
return status;
|
||||
return status_;
|
||||
default:
|
||||
std::cout << "Attempt to read invalid disk controller register.\n";
|
||||
std::abort();
|
||||
@@ -222,37 +215,153 @@ Word DiskControllerMemDevice::read(Addr a) {
|
||||
void DiskControllerMemDevice::write(Addr a, Word w) {
|
||||
switch (a / 8) {
|
||||
case 0:
|
||||
if (w <= disks.size()) {
|
||||
curDisk = w;
|
||||
status = OK;
|
||||
if (w <= disks_.size()) {
|
||||
curDisk_ = w;
|
||||
status_ = OK;
|
||||
} else {
|
||||
status = INVALID_DISK;
|
||||
status_ = INVALID_DISK;
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
if (w < disks[curDisk].blocks) {
|
||||
curBlock = w;
|
||||
if (w < disks_[curDisk_].blocks) {
|
||||
curBlock_ = w;
|
||||
} else {
|
||||
status = INVALID_BLOCK;
|
||||
status_ = INVALID_BLOCK;
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
nBlocks = w >= disks[curDisk].blocks ? disks[curDisk].blocks - 1 : w;
|
||||
status = OK;
|
||||
nBlocks_ = w >= disks_[curDisk_].blocks ? disks_[curDisk_].blocks - 1 : w;
|
||||
status_ = OK;
|
||||
break;
|
||||
case 3:
|
||||
physAddr = w;
|
||||
status = OK;
|
||||
physAddr_ = w;
|
||||
status_ = OK;
|
||||
break;
|
||||
case 4:
|
||||
if (w == 0) {
|
||||
} else {
|
||||
}
|
||||
std::cout << "TODO: Implement disk read and write!\n";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
RAM::RAM(uint32_t num_pages, uint32_t page_size)
|
||||
: page_bits_(log2ceil(page_size)) {
|
||||
assert(page_size >= 4);
|
||||
assert(ispow2(page_size));
|
||||
mem_.resize(num_pages, NULL);
|
||||
uint64_t sizel = uint64_t(mem_.size()) << page_bits_;
|
||||
size_ = (sizel <= 0xFFFFFFFF) ? sizel : 0xffffffff;
|
||||
}
|
||||
|
||||
RAM::~RAM() {
|
||||
for (auto& page : mem_) {
|
||||
delete[] page;
|
||||
}
|
||||
}
|
||||
|
||||
void RAM::clear() {
|
||||
for (auto& page : mem_) {
|
||||
delete[] page;
|
||||
page = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
Size RAM::size() const {
|
||||
return size_;
|
||||
}
|
||||
|
||||
uint8_t *RAM::get(uint32_t address) {
|
||||
uint32_t page_size = 14 << page_bits_;
|
||||
uint32_t page_index = address >> page_bits_;
|
||||
uint32_t byte_offset = address & ((1 << page_bits_) - 1);
|
||||
|
||||
uint8_t* &page = mem_.at(page_index);
|
||||
if (page == NULL) {
|
||||
uint8_t *ptr = new uint8_t[page_size];
|
||||
for (uint32_t i = 0; i < (page_size / 4); ++i) {
|
||||
((uint32_t*)ptr)[i] = 0xddccbbaa;
|
||||
}
|
||||
page = ptr;
|
||||
}
|
||||
return page + byte_offset;
|
||||
}
|
||||
|
||||
void RAM::read(uint32_t address, uint32_t length, uint8_t *data) {
|
||||
for (unsigned i = 0; i < length; i++) {
|
||||
data[i] = *this->get(address + i);
|
||||
}
|
||||
}
|
||||
|
||||
void RAM::write(uint32_t address, uint32_t length, uint8_t *data) {
|
||||
for (unsigned i = 0; i < length; i++) {
|
||||
*this->get(address + i) = data[i];
|
||||
}
|
||||
}
|
||||
|
||||
Byte *RAM::base() {
|
||||
return (Byte *)this->get(0);
|
||||
}
|
||||
|
||||
void RAM::getBlock(uint32_t address, uint8_t *data) {
|
||||
uint32_t block_number = address & 0xffffff00; // To zero out block offset
|
||||
uint32_t bytes_num = 256;
|
||||
this->read(block_number, bytes_num, data);
|
||||
}
|
||||
|
||||
void RAM::getWord(uint32_t address, uint32_t *data) {
|
||||
data[0] = 0;
|
||||
|
||||
uint8_t first = *get(address + 0);
|
||||
uint8_t second = *get(address + 1);
|
||||
uint8_t third = *get(address + 2);
|
||||
uint8_t fourth = *get(address + 3);
|
||||
|
||||
data[0] = (data[0] << 0) | fourth;
|
||||
data[0] = (data[0] << 8) | third;
|
||||
data[0] = (data[0] << 8) | second;
|
||||
data[0] = (data[0] << 8) | first;
|
||||
}
|
||||
|
||||
void RAM::writeWord(uint32_t address, uint32_t *data) {
|
||||
uint32_t data_to_write = *data;
|
||||
uint32_t byte_mask = 0xFF;
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
*this->get(address + i) = data_to_write & byte_mask;
|
||||
data_to_write = data_to_write >> 8;
|
||||
}
|
||||
}
|
||||
|
||||
void RAM::writeHalf(uint32_t address, uint32_t *data) {
|
||||
uint32_t data_to_write = *data;
|
||||
uint32_t byte_mask = 0xFF;
|
||||
|
||||
for (int i = 0; i < 2; i++) {
|
||||
*this->get(address + i) = data_to_write & byte_mask;
|
||||
data_to_write = data_to_write >> 8;
|
||||
}
|
||||
}
|
||||
|
||||
void RAM::writeByte(uint32_t address, uint32_t *data) {
|
||||
uint32_t data_to_write = *data;
|
||||
uint32_t byte_mask = 0xFF;
|
||||
|
||||
*this->get(address) = data_to_write & byte_mask;
|
||||
data_to_write = data_to_write >> 8;
|
||||
}
|
||||
|
||||
void RAM::write(Addr addr, Word w) {
|
||||
uint32_t word = (uint32_t)w;
|
||||
writeWord(addr, &word);
|
||||
}
|
||||
|
||||
Word RAM::read(Addr addr) {
|
||||
uint32_t w;
|
||||
getWord(addr, &w);
|
||||
return (Word)w;
|
||||
}
|
||||
|
||||
static uint32_t hti_old(char c) {
|
||||
if (c >= 'A' && c <= 'F')
|
||||
return c - 'A' + 10;
|
||||
@@ -269,58 +378,18 @@ static uint32_t hToI_old(char *c, uint32_t size) {
|
||||
return value;
|
||||
}
|
||||
|
||||
void RAM::loadHexImpl(std::string path) {
|
||||
void RAM::loadHexImage(std::string path) {
|
||||
this->clear();
|
||||
FILE *fp = fopen(&path[0], "r");
|
||||
if (fp == 0) {
|
||||
std::cout << path << " not found" << std::endl;
|
||||
}
|
||||
|
||||
//Preload 0x0 <-> 0x80000000 jumps
|
||||
((uint32_t *)this->get(0))[0] = 0xf1401073;
|
||||
((uint32_t *)this->get(0))[1] = 0xf1401073;
|
||||
((uint32_t *)this->get(0))[2] = 0x30101073;
|
||||
((uint32_t *)this->get(0))[3] = 0x800000b7;
|
||||
((uint32_t *)this->get(0))[4] = 0x000080e7;
|
||||
|
||||
((uint32_t *)this->get(0x80000000))[0] = 0x00000097;
|
||||
|
||||
((uint32_t *)this->get(0xb0000000))[0] = 0x01C02023;
|
||||
|
||||
((uint32_t *)this->get(0xf00fff10))[0] = 0x12345678;
|
||||
|
||||
((uint32_t *)this->get(0x70000000))[0] = 0x00008067;
|
||||
|
||||
{
|
||||
uint32_t init_addr = 0x70000004;
|
||||
for (int off = 0; off < 1024; off += 4) {
|
||||
uint32_t new_addr = init_addr + off;
|
||||
((uint32_t *)this->get(new_addr))[0] = 0x00000000;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
uint32_t init_addr = 0x71000000;
|
||||
for (int off = 0; off < 1024; off += 4) {
|
||||
uint32_t new_addr = init_addr + off;
|
||||
((uint32_t *)this->get(new_addr))[0] = 0x00000000;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
uint32_t init_addr = 0x72000000;
|
||||
for (int off = 0; off < 1024; off += 4) {
|
||||
uint32_t new_addr = init_addr + off;
|
||||
((uint32_t *)this->get(new_addr))[0] = 0x00000000;
|
||||
}
|
||||
}
|
||||
|
||||
fseek(fp, 0, SEEK_END);
|
||||
uint32_t size = ftell(fp);
|
||||
fseek(fp, 0, SEEK_SET);
|
||||
char *content = new char[size];
|
||||
int x = fread(content, 1, size, fp);
|
||||
|
||||
if (!x) {
|
||||
std::cout << "COULD NOT READ FILE\n";
|
||||
std::abort();
|
||||
@@ -328,7 +397,7 @@ void RAM::loadHexImpl(std::string path) {
|
||||
|
||||
int offset = 0;
|
||||
char *line = content;
|
||||
// std::cout << "WHTA\n";
|
||||
|
||||
while (1) {
|
||||
if (line[0] == ':') {
|
||||
uint32_t byteCount = hToI_old(line + 1, 2);
|
||||
@@ -338,32 +407,25 @@ void RAM::loadHexImpl(std::string path) {
|
||||
case 0:
|
||||
for (uint32_t i = 0; i < byteCount; i++) {
|
||||
unsigned add = nextAddr + i;
|
||||
*(this->get(add)) = hToI_old(line + 9 + i * 2, 2);
|
||||
// std::cout << "lhi: Address: " << std::hex <<(add) << "\tValue: " << std::hex << hToI_old(line + 9 + i * 2, 2) << std::endl;
|
||||
*this->get(add) = hToI_old(line + 9 + i * 2, 2);
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
// cout << offset << std::endl;
|
||||
offset = hToI_old(line + 9, 4) << 4;
|
||||
break;
|
||||
case 4:
|
||||
// cout << offset << std::endl;
|
||||
offset = hToI_old(line + 9, 4) << 16;
|
||||
break;
|
||||
default:
|
||||
// cout << "??? " << key << std::endl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
while (*line != '\n' && size != 0) {
|
||||
line++;
|
||||
size--;
|
||||
}
|
||||
|
||||
if (size <= 1)
|
||||
break;
|
||||
|
||||
line++;
|
||||
size--;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user