/*
This file is part of jpcsp.
Jpcsp is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Jpcsp is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Jpcsp. If not, see <http://www.gnu.org/licenses/>.
*/
package jpcsp.graphics;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import java.util.HashMap;
import org.apache.log4j.Logger;
import jpcsp.Memory;
import jpcsp.Allegrex.compiler.RuntimeContext;
import jpcsp.graphics.RE.IRenderingEngine;
import jpcsp.memory.IMemoryReader;
import jpcsp.memory.MemoryReader;
import jpcsp.util.Utilities;
/**
* @author gid15
*
*/
public class VertexBuffer {
private static Logger log = VideoEngine.log;
private int bufferId = -1;
private int bufferAddress;
private int bufferLength;
private int stride;
private int[] cachedMemory;
private ByteBuffer cachedBuffer;
private int cachedBufferOffset;
private HashMap<Integer, Integer> addressAlreadyChecked = new HashMap<Integer, Integer>();
private AddressRange[] dirtyRanges = new AddressRange[0];
private int numberDirtyRanges = 0;
private boolean reloadBufferDataPending = false;
private static final int bufferUsage = IRenderingEngine.RE_DYNAMIC_DRAW;
private static final int bufferTarget = IRenderingEngine.RE_ARRAY_BUFFER;
private static final boolean workaroundBufferDataBug = true;
private static class AddressRange {
public int address;
public int length;
public AddressRange() {
}
public void setRange(int address, int length) {
this.address = address;
this.length = length;
}
@Override
public String toString() {
return String.format("AddressRange[0x%08X-0x%08X, length %d]", address, address + length, length);
}
}
public VertexBuffer(int address, int stride) {
bufferAddress = Memory.normalizeAddress(address);
bufferLength = 0;
this.stride = stride;
}
public void bind(IRenderingEngine re) {
if (bufferId == -1) {
bufferId = re.genBuffer();
}
re.bindBuffer(bufferTarget, bufferId);
}
private int getBufferAlignment(Buffer buffer, int address) {
if ((address & 3) == 0) {
return 0;
}
if (buffer instanceof IntBuffer || buffer instanceof FloatBuffer) {
return address & 3;
} else if (buffer instanceof ShortBuffer) {
return address & 1;
}
return 0;
}
private void loadFromMemory(int address, int length) {
if (length > 0) {
copyToCachedMemory(address, length);
Buffer buffer = Memory.getInstance().getBuffer(address, length);
int bufferAlignment = getBufferAlignment(buffer, address);
position(address, bufferAlignment);
Utilities.putBuffer(cachedBuffer, buffer, ByteOrder.LITTLE_ENDIAN, length + bufferAlignment);
}
}
private boolean extend(Buffer buffer, int address, int length) {
final boolean overflowBottom = address < bufferAddress;
final boolean overflowTop = address + length > bufferAddress + bufferLength;
boolean extended = false;
if (!overflowBottom && !overflowTop) {
// Most common case: the buffer is fitting
} else if (bufferLength == 0 || (overflowBottom && overflowTop)) {
// Create a new buffer
cachedBufferOffset = getBufferAlignment(buffer, address);
// Always allocate 3 additional bytes at the end to allow copy
// from IntBuffer without running into a buffer overflow
final int alignmentPaddingEnd = 3;
cachedBuffer = ByteBuffer.allocateDirect(length + cachedBufferOffset + alignmentPaddingEnd).order(ByteOrder.LITTLE_ENDIAN);
bufferAddress = address;
bufferLength = length;
cachedMemory = new int[bufferLength >> 2];
// The buffer has been resized: its content is lost, reload it
reloadBufferDataPending = true;
extended = true;
} else if (overflowBottom) {
// Extend the buffer to the bottom
cachedBufferOffset = getBufferAlignment(buffer, address);
int extendLength = bufferAddress - address + cachedBufferOffset;
ByteBuffer newBuffer = ByteBuffer.allocateDirect(extendLength + cachedBuffer.capacity()).order(ByteOrder.LITTLE_ENDIAN);
newBuffer.position(extendLength);
cachedBuffer.clear();
newBuffer.put(cachedBuffer);
newBuffer.rewind();
cachedBuffer = newBuffer;
bufferLength += extendLength;
int[] newCachedMemory = new int[bufferLength >> 2];
System.arraycopy(cachedMemory, 0, newCachedMemory, extendLength >> 2, cachedMemory.length);
cachedMemory = newCachedMemory;
bufferAddress = address;
loadFromMemory(bufferAddress + length, extendLength - length);
// The buffer has been resized: its content is lost, reload it
reloadBufferDataPending = true;
extended = true;
} else if (overflowTop) {
// Extend the buffer to the top
int extendLength = address + length - (bufferAddress + bufferLength);
ByteBuffer newBuffer = ByteBuffer.allocateDirect(extendLength + cachedBuffer.capacity()).order(ByteOrder.LITTLE_ENDIAN);
cachedBuffer.clear();
newBuffer.put(cachedBuffer);
newBuffer.rewind();
cachedBuffer = newBuffer;
int oldBufferEnd = bufferAddress + bufferLength;
bufferLength += extendLength;
int[] newCachedMemory = new int[bufferLength >> 2];
System.arraycopy(cachedMemory, 0, newCachedMemory, 0, cachedMemory.length);
cachedMemory = newCachedMemory;
loadFromMemory(oldBufferEnd, address - oldBufferEnd);
// The buffer has been resized: its content is lost, reload it
reloadBufferDataPending = true;
extended = true;
}
return extended;
}
private void position(int address) {
cachedBuffer.clear();
cachedBuffer.position(getBufferOffset(address) + cachedBufferOffset);
}
private void position(int address, int bufferAlignment) {
position(address - bufferAlignment);
}
private void copyToCachedMemory(int address, int length) {
int offset = getBufferOffset(address) >> 2;
int n = length >> 2;
if (RuntimeContext.hasMemoryInt()) {
System.arraycopy(RuntimeContext.getMemoryInt(), address >> 2, cachedMemory, offset, n);
} else {
IMemoryReader memoryReader = MemoryReader.getMemoryReader(address, length, 4);
for (int i = 0; i < n; i++) {
cachedMemory[offset + i] = memoryReader.readNext();
}
}
}
private void checkDirty(IRenderingEngine re) {
if (reloadBufferDataPending) {
bind(re);
position(bufferAddress);
re.setBufferData(bufferTarget, cachedBuffer.remaining(), cachedBuffer, bufferUsage);
reloadBufferDataPending = false;
numberDirtyRanges = 0;
} else if (numberDirtyRanges > 0) {
bind(re);
for (int i = 0; i < numberDirtyRanges; i++) {
position(dirtyRanges[i].address);
re.setBufferSubData(bufferTarget, cachedBuffer.position(), dirtyRanges[i].length, cachedBuffer);
}
numberDirtyRanges = 0;
}
}
private boolean cachedMemoryEquals(int address, int length) {
IMemoryReader memoryReader = MemoryReader.getMemoryReader(address, length, 4);
int n = length >> 2;
int offset = getBufferOffset(address) >> 2;
for (int i = 0; i < n; i++) {
if (cachedMemory[offset + i] != memoryReader.readNext()) {
if (log.isTraceEnabled()) {
log.trace(String.format("VertexBuffer.cachedMemoryEquals(0x%08X, %d): are not equal", address, length));
}
return false;
}
}
if (log.isTraceEnabled()) {
log.trace(String.format("VertexBuffer.cachedMemoryEquals(0x%08X, %d): are equal", address, length));
}
return true;
}
private void addDirtyRange(int address, int length) {
for (int i = 0; i < numberDirtyRanges; i++) {
if (dirtyRanges[i].address == address) {
if (length > dirtyRanges[i].length) {
dirtyRanges[i].length = length;
}
return;
}
}
if (numberDirtyRanges >= dirtyRanges.length) {
// Extend dirtyRanges array
AddressRange[] newDirtyRanges = new AddressRange[dirtyRanges.length + 10];
System.arraycopy(dirtyRanges, 0, newDirtyRanges, 0, dirtyRanges.length);
for (int i = dirtyRanges.length; i < newDirtyRanges.length; i++) {
newDirtyRanges[i] = new AddressRange();
}
dirtyRanges = newDirtyRanges;
}
dirtyRanges[numberDirtyRanges].setRange(address, length);
numberDirtyRanges++;
}
public synchronized void preLoad(Buffer buffer, int address, int length) {
load(null, buffer, address, length);
}
public synchronized void load(IRenderingEngine re, Buffer buffer, int address, int length) {
address = Memory.normalizeAddress(address);
if (log.isTraceEnabled()) {
log.trace(String.format("VertexBuffer.load(0x%08X, %d) in %s", address, length, this.toString()));
}
if (!addressAlreadyChecked(address, length)) {
boolean extended = extend(buffer, address, length);
// Check if the memory content has changed
if (extended || !cachedMemoryEquals(address, length)) {
int bufferAlignment = getBufferAlignment(buffer, address);
position(address, bufferAlignment);
Utilities.putBuffer(cachedBuffer, buffer, ByteOrder.LITTLE_ENDIAN, length + bufferAlignment);
buffer.rewind();
if (re != null) {
if (log.isTraceEnabled()) {
log.trace(String.format("VertexBuffer reload buffer 0x%08X, %d, extended=%b", address, length, extended));
}
// No need to update the sub data if the complete buffer has been reloaded...
boolean updateSubData = !reloadBufferDataPending;
checkDirty(re);
if (updateSubData) {
position(address);
bind(re);
re.setBufferSubData(bufferTarget, cachedBuffer.position(), length, cachedBuffer);
}
} else {
addDirtyRange(address, length);
}
copyToCachedMemory(address, length);
} else if (re != null) {
checkDirty(re);
// Here the buffer data should not need to be reloaded, it is matching
// the previous data. However, due to a driver bug (?), the buffer data
// has sometimes been corrupted in the GPU. This problem seems to happen
// only under some circumstances but I was not able to identify the exact
// conditions. Just reloading a single byte of the buffer restores
// the correct data in the buffer. This is why I assumed this is a driver bug.
// This workaround could however completely break the performance of the vertex
// cache!
if (workaroundBufferDataBug) {
bind(re);
position(bufferAddress);
re.setBufferSubData(bufferTarget, cachedBuffer.position(), 1, cachedBuffer);
}
}
setAddressAlreadyChecked(address, length);
} else if (re != null) {
if (log.isTraceEnabled()) {
log.trace(String.format("VertexBuffer address already checked 0x%08X, %d", address, length));
}
checkDirty(re);
}
}
public synchronized void delete(IRenderingEngine re) {
if (bufferId != -1) {
re.deleteBuffer(bufferId);
bufferId = -1;
}
bufferLength = 0;
bufferAddress = 0;
stride = 0;
cachedBuffer = null;
cachedMemory = null;
}
public int getBufferOffset(int address) {
address = Memory.normalizeAddress(address);
return address - bufferAddress;
}
public boolean isAddressInside(int address, int length, int gapSize) {
address = Memory.normalizeAddress(address);
int endAddress = address + length;
int startBuffer = bufferAddress - gapSize;
int endBuffer = bufferAddress + bufferLength + gapSize;
// start address inside the buffer
if (startBuffer <= address && address < endBuffer) {
return true;
}
// end address inside the buffer
if (startBuffer <= endAddress && endAddress < endBuffer) {
return true;
}
// start & end address including the buffer
if (address < startBuffer && endBuffer < endAddress) {
return true;
}
return false;
}
public synchronized void resetAddressAlreadyChecked() {
addressAlreadyChecked.clear();
}
private boolean addressAlreadyChecked(int address, int length) {
Integer checkedLength = addressAlreadyChecked.get(address);
if (checkedLength == null) {
return false;
}
return checkedLength.intValue() >= length;
}
private void setAddressAlreadyChecked(int address, int length) {
addressAlreadyChecked.put(address, length);
}
public int getStride() {
return stride;
}
public int getLength() {
return bufferLength;
}
public int getId() {
return bufferId;
}
@Override
public String toString() {
return String.format("VertexBuffer[0x%08X-0x%08X, length %d, stride %d, id %d]", bufferAddress, bufferAddress + bufferLength, bufferLength, stride, bufferId);
}
}