Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for "reflow"ing the Terminal buffer #4741

Merged
merged 54 commits into from
Mar 13, 2020
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
b5c8c85
let's first move reflowing to the text buffer
zadjii-msft Jan 13, 2020
9aec694
add a doc comment because I'm not a barbarian
zadjii-msft Jan 13, 2020
1a2654d
Try to wrap the line properly with conpty
zadjii-msft Jan 13, 2020
aae6ce6
This works, fixing the ECH at end of wrapped line bug
zadjii-msft Jan 14, 2020
416be46
This is some cleanup, almost ready for PR but I need to write tests w…
zadjii-msft Jan 14, 2020
7f341a2
Merge branch 'master' into dev/migrie/f/conpty-wrapping-003
zadjii-msft Jan 27, 2020
edeb346
I think this is all I need to support wrapped lines in the Terminal
zadjii-msft Jan 27, 2020
ce3138c
Let's pull all the test fixes into their own file
zadjii-msft Jan 28, 2020
4a7f2e4
Merge branch 'dev/migrie/b/just-conpty-test-fixes' into dev/migrie/f/…
zadjii-msft Jan 28, 2020
bfde821
A simple test for wrapped lines
zadjii-msft Jan 28, 2020
e000388
add a roundtrip test
zadjii-msft Jan 28, 2020
c040a82
I've found a bug with the line wrapping, going to go update the Paint…
zadjii-msft Jan 28, 2020
0755fd7
This is polished for PR, ready to go in after #4382
zadjii-msft Jan 28, 2020
86623f5
Add PaintCursor tracing
zadjii-msft Jan 29, 2020
7fd5d51
This actually fixes this bug - different terminals EOL wrap different…
zadjii-msft Jan 29, 2020
9b6554b
Add some comments for PR
zadjii-msft Jan 29, 2020
0a98cce
Try adding a test, but I can't get the test to repro the original beh…
zadjii-msft Jan 29, 2020
40b4966
Revert "Try adding a test, but I can't get the test to repro the orig…
zadjii-msft Jan 29, 2020
e0d251c
Merge remote-tracking branch 'origin/master' into dev/migrie/b/1245-I…
zadjii-msft Jan 29, 2020
b3de042
remove some old TODO comments
zadjii-msft Jan 30, 2020
96642de
remove other dead code for PR
zadjii-msft Jan 30, 2020
5a72af9
Merge branch 'dev/migrie/f/conpty-wrapping-003' into dev/migrie/f/ref…
zadjii-msft Jan 30, 2020
1788cb1
Merge branch 'dev/migrie/b/1245-I-actually-did-it-this-time' into dev…
zadjii-msft Jan 30, 2020
74a5283
ResizeWithReflow the Terminal buffer
zadjii-msft Jan 30, 2020
9580715
wait why does this work so well
zadjii-msft Jan 30, 2020
edea9a3
Cleanup for review - this is a _great_ fix for #3490 as well as #1465
zadjii-msft Jan 30, 2020
097b62c
Merge remote-tracking branch 'origin/master' into dev/migrie/f/reflow…
zadjii-msft Jan 31, 2020
2e815c8
fix tests
zadjii-msft Jan 31, 2020
de9911d
Merge remote-tracking branch 'origin/master' into dev/migrie/f/reflow…
zadjii-msft Feb 7, 2020
e653713
Merge remote-tracking branch 'origin/master' into dev/migrie/f/reflow…
zadjii-msft Feb 11, 2020
1d87f66
Merge branch 'master' into dev/migrie/f/reflow-buffer-on-resize
zadjii-msft Feb 28, 2020
0c91a9b
@ our static analysis build: you're wrong here, but fine
zadjii-msft Feb 28, 2020
0f283b4
Merge remote-tracking branch 'origin/master' into dev/migrie/f/reflow…
zadjii-msft Mar 2, 2020
2ef2d88
add safemath for carlos
zadjii-msft Mar 2, 2020
cc7b2d3
okay I'll actually build the SA locally
zadjii-msft Mar 2, 2020
a8913ce
Squash merge of dev/migrie/f/resize-quirk
zadjii-msft Mar 5, 2020
e0626c8
audit mode is hard sometimes
zadjii-msft Mar 5, 2020
4c368c3
delete some old dead code
zadjii-msft Mar 5, 2020
4e3d008
Merge remote-tracking branch 'origin/master' into dev/migrie/f/reflow…
zadjii-msft Mar 6, 2020
6070095
Fix wrapping lines far too frequently
zadjii-msft Mar 6, 2020
7cca547
Some minor comments from Michael
zadjii-msft Mar 6, 2020
1044d49
Merge remote-tracking branch 'origin/master' into dev/migrie/f/reflow…
zadjii-msft Mar 10, 2020
277f280
Enable conpty to be resized even when headed
zadjii-msft Mar 10, 2020
7a40fe3
These bugs were twofold
zadjii-msft Mar 10, 2020
a3a9464
When we increase the width of the buffer, always use the old viewport…
zadjii-msft Mar 10, 2020
4ca280c
When we change the width at all, use the top, not the bottom.
zadjii-msft Mar 10, 2020
077f678
This seems like it finally does it
zadjii-msft Mar 10, 2020
85efaa7
git merge --squash dev/migrie/f/more-reflow-experiments
zadjii-msft Mar 11, 2020
9127e1c
Merge remote-tracking branch 'origin/master' into dev/migrie/f/reflow…
zadjii-msft Mar 12, 2020
61ccc28
fix the tests
zadjii-msft Mar 12, 2020
aff3cc5
someday I"ll actually save all the files with changes in them before …
zadjii-msft Mar 12, 2020
0532c66
fix the renderer test
zadjii-msft Mar 12, 2020
5394b30
make this a reference to an optional. These tests better pass...
zadjii-msft Mar 12, 2020
7ecd3c6
Merge remote-tracking branch 'origin/master' into dev/migrie/f/reflow…
DHowett Mar 12, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions src/buffer/out/textBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1900,9 +1900,14 @@ std::string TextBuffer::GenRTF(const TextAndColor& rows, const int fontHeightPoi
// Arguments:
// - oldBuffer - the text buffer to copy the contents FROM
// - newBuffer - the text buffer to copy the contents TO
// - lastCharacterViewport - Optional. If the caller knows that the last
// nonspace character is in a particular Viewport, the caller can provide this
// parameter as an optimization, as opposed to searching the entire buffer.
// Return Value:
// - S_OK if we successfully copied the contents to the new buffer, otherwise an appropriate HRESULT.
HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer, TextBuffer& newBuffer)
HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer,
TextBuffer& newBuffer,
const std::optional<Viewport> lastCharacterViewport) noexcept
{
Cursor& oldCursor = oldBuffer.GetCursor();
Cursor& newCursor = newBuffer.GetCursor();
Expand All @@ -1914,10 +1919,12 @@ HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer, TextBuffer& newBuffer)
// place the new cursor back on the equivalent character in
// the new buffer.
const COORD cOldCursorPos = oldCursor.GetPosition();
const COORD cOldLastChar = oldBuffer.GetLastNonSpaceCharacter();
const COORD cOldLastChar = lastCharacterViewport.has_value() ?
oldBuffer.GetLastNonSpaceCharacter(lastCharacterViewport.value()) :
oldBuffer.GetLastNonSpaceCharacter();
zadjii-msft marked this conversation as resolved.
Show resolved Hide resolved

short const cOldRowsTotal = cOldLastChar.Y + 1;
short const cOldColsTotal = oldBuffer.GetSize().Width();
const short cOldRowsTotal = cOldLastChar.Y + 1;
const short cOldColsTotal = oldBuffer.GetSize().Width();

COORD cNewCursorPos = { 0 };
bool fFoundCursorPos = false;
Expand Down
2 changes: 1 addition & 1 deletion src/buffer/out/textBuffer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ class TextBuffer final
const std::wstring_view fontFaceName,
const COLORREF backgroundColor);

static HRESULT Reflow(TextBuffer& oldBuffer, TextBuffer& newBuffer);
static HRESULT Reflow(TextBuffer& oldBuffer, TextBuffer& newBuffer, const std::optional<Microsoft::Console::Types::Viewport> lastCharacterViewport = std::nullopt) noexcept;

private:
std::deque<ROW> _storage;
Expand Down
51 changes: 47 additions & 4 deletions src/cascadia/TerminalCore/Terminal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,50 @@ void Terminal::UpdateSettings(winrt::Microsoft::Terminal::Settings::ICoreSetting

const short newBufferHeight = viewportSize.Y + _scrollbackLines;
COORD bufferSize{ viewportSize.X, newBufferHeight };
RETURN_IF_FAILED(_buffer->ResizeTraditional(bufferSize));

auto proposedTop = oldTop;
const auto newView = Viewport::FromDimensions({ 0, proposedTop }, viewportSize);
// First allocate a new text buffer to take the place of the current one.
std::unique_ptr<TextBuffer> newTextBuffer;
try
{
newTextBuffer = std::make_unique<TextBuffer>(bufferSize,
_buffer->GetCurrentAttributes(),
0, // temporarily set size to 0 so it won't render.
_buffer->GetRenderTarget());
}
CATCH_RETURN();

RETURN_IF_FAILED(TextBuffer::Reflow(*_buffer.get(), *newTextBuffer.get(), _mutableViewport));

// However conpty resizes a little oddly - if the height decreased, and
// there were blank lines at the bottom, those lines will get trimmed.
// If there's not blank lines, then the top will get "shifted down",
// moving the top line into scrollback.
// See GH#3490 for more details.

// If the final position in the buffer is on the bottom row of the new
// viewport, then we're going to need to move the top down. Otherwise,
// move the bottom up.
const auto dy = viewportSize.Y - oldDimensions.Y;
const COORD oldCursorPos = _buffer->GetCursor().GetPosition();

#pragma warning(push)
#pragma warning(disable : 26496) // cpp core checks wants this const, but it's assigned immediately below...
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#pragma warning (suppress: 26496)
should also work if you want a one liner instead of push/disable/pop.

COORD oldLastChar = oldCursorPos;
try
{
oldLastChar = _buffer->GetLastNonSpaceCharacter(_mutableViewport);
}
CATCH_LOG();
#pragma warning(pop)

const auto maxRow = std::max(oldLastChar.Y, oldCursorPos.Y);

const bool beforeLastRowOfView = maxRow < _mutableViewport.BottomInclusive();
const auto adjustment = beforeLastRowOfView ? 0 : std::max(0, -dy);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A little more commenting/ascii art/etc would probably do a future person some good here.


auto proposedTop = oldTop + adjustment;

const auto newView = Viewport::FromDimensions({ 0, ::base::saturated_cast<short>(proposedTop) }, viewportSize);
const auto proposedBottom = newView.BottomExclusive();
// If the new bottom would be below the bottom of the buffer, then slide the
// top up so that we'll still fit within the buffer.
Expand All @@ -190,7 +230,10 @@ void Terminal::UpdateSettings(winrt::Microsoft::Terminal::Settings::ICoreSetting
proposedTop -= (proposedBottom - bufferSize.Y);
}

_mutableViewport = Viewport::FromDimensions({ 0, proposedTop }, viewportSize);
_mutableViewport = Viewport::FromDimensions({ 0, ::base::saturated_cast<short>(proposedTop) }, viewportSize);

_buffer.swap(newTextBuffer);

_scrollOffset = 0;
_NotifyScrollEvent();

Expand Down
229 changes: 229 additions & 0 deletions src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ class TerminalCoreUnitTests::ConptyRoundtripTests final

TEST_METHOD(MoveCursorAtEOL);

TEST_METHOD(TestResizeHeight);

private:
bool _writeCallback(const char* const pch, size_t const cch);
void _flushFirstFrame();
Expand Down Expand Up @@ -550,6 +552,7 @@ void ConptyRoundtripTests::TestExactWrappingWithSpaces()
auto& gci = g.getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer();
auto& hostSm = si.GetStateMachine();

auto& hostTb = si.GetTextBuffer();
auto& termTb = *term->_buffer;
const auto initialTermView = term->GetViewport();
Expand Down Expand Up @@ -620,6 +623,7 @@ void ConptyRoundtripTests::MoveCursorAtEOL()
auto& gci = g.getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer();
auto& hostSm = si.GetStateMachine();

auto& hostTb = si.GetTextBuffer();
auto& termTb = *term->_buffer;
_flushFirstFrame();
Expand Down Expand Up @@ -673,6 +677,230 @@ void ConptyRoundtripTests::MoveCursorAtEOL()
verifyData1(termTb);
}

void ConptyRoundtripTests::TestResizeHeight()
{
// This test class is _60_ tests to ensure that resizing the terminal works
zadjii-msft marked this conversation as resolved.
Show resolved Hide resolved
// with conpty correctly. There's a lot of min/maxing in expressions here,
// to account for the sheer number of cases here, and that we have to handle
// both resizing larger and smaller all in one test.

BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
TEST_METHOD_PROPERTY(L"Data:dx", L"{-1, 0, 1}")
TEST_METHOD_PROPERTY(L"Data:dy", L"{-10, -1, 0, 1, 10}")
TEST_METHOD_PROPERTY(L"Data:printedRows", L"{1, 10, 50, 200}")
END_TEST_METHOD_PROPERTIES()
int dx, dy;
int printedRows;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"dx", dx), L"change in width of buffer");
VERIFY_SUCCEEDED(TestData::TryGetValue(L"dy", dy), L"change in height of buffer");
VERIFY_SUCCEEDED(TestData::TryGetValue(L"printedRows", printedRows), L"Number of rows of text to print");

_checkConptyOutput = false;

auto& g = ServiceLocator::LocateGlobals();
auto& renderer = *g.pRender;
auto& gci = g.getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer();
auto& hostSm = si.GetStateMachine();
auto* hostTb = &si.GetTextBuffer();
auto* termTb = term->_buffer.get();
const auto initialHostView = si.GetViewport();
const auto initialTermView = term->GetViewport();
const auto initialTerminalBufferHeight = term->GetTextBuffer().GetSize().Height();

VERIFY_ARE_EQUAL(0, initialHostView.Top());
VERIFY_ARE_EQUAL(TerminalViewHeight, initialHostView.BottomExclusive());
VERIFY_ARE_EQUAL(0, initialTermView.Top());
VERIFY_ARE_EQUAL(TerminalViewHeight, initialTermView.BottomExclusive());

Log::Comment(NoThrowString().Format(
L"Print %d lines of output, which will scroll the viewport", printedRows));

for (auto i = 0; i < printedRows; i++)
{
// This looks insane, but this expression is carefully crafted to give
// us only printable characters, starting with `!` (0n33).
// Similar statements are used elsewhere throughout this test.
auto wstr = std::wstring(1, static_cast<wchar_t>((i) % 93) + 33);
hostSm.ProcessString(wstr);
hostSm.ProcessString(L"\r\n");
}

// Conpty doesn't have a scrollback, it's view's origin is always 0,0
const auto secondHostView = si.GetViewport();
VERIFY_ARE_EQUAL(0, secondHostView.Top());
VERIFY_ARE_EQUAL(TerminalViewHeight, secondHostView.BottomExclusive());

VERIFY_SUCCEEDED(renderer.PaintFrame());

const auto secondTermView = term->GetViewport();
// If we've printed more lines than the height of the buffer, then we're
// expecting the viewport to have moved down. Otherwise, the terminal's
// viewport will stay at 0,0.
const auto expectedTerminalViewBottom = std::max(std::min(::base::saturated_cast<short>(printedRows + 1),
term->GetBufferHeight()),
term->GetViewport().Height());

VERIFY_ARE_EQUAL(expectedTerminalViewBottom, secondTermView.BottomExclusive());
VERIFY_ARE_EQUAL(expectedTerminalViewBottom - initialTermView.Height(), secondTermView.Top());

auto verifyTermData = [&expectedTerminalViewBottom, &printedRows, this, &initialTerminalBufferHeight](TextBuffer& termTb, const int resizeDy = 0) {
// Some number of lines of text were lost from the scrollback. The
// number of lines lost will be determined by whichever of the initial
// or current buffer is smaller.
const auto numLostRows = std::max(0,
printedRows - std::min(term->GetTextBuffer().GetSize().Height(), initialTerminalBufferHeight) + 1);

const auto rowsWithText = std::min(::base::saturated_cast<short>(printedRows),
expectedTerminalViewBottom) -
1 + std::min(resizeDy, 0);

for (short row = 0; row < rowsWithText; row++)
{
SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures);
auto iter = termTb.GetCellDataAt({ 0, row });
const wchar_t expectedChar = static_cast<wchar_t>((row + numLostRows) % 93) + 33;

auto expectedString = std::wstring(1, expectedChar);

if (iter->Chars() != expectedString)
{
Log::Comment(NoThrowString().Format(L"row [%d] was mismatched", row));
}
VERIFY_ARE_EQUAL(expectedString, (iter++)->Chars());
VERIFY_ARE_EQUAL(L" ", (iter)->Chars());
}
};
auto verifyHostData = [&si, &initialHostView, &printedRows](TextBuffer& hostTb, const int resizeDy = 0) {
const auto hostView = si.GetViewport();

// In the host, there are two regions we're interested in:

// 1. the first section of the buffer with the output in it. Before
// we're resized, this will be filled with one character on each row.
// 2. The second area below the first that's empty (filled with spaces).
// Initially, this is only one row.
// After we resize, different things will happen.
// * If we decrease the height of the buffer, the characters in the
// buffer will all move _up_ the same number of rows. We'll want to
// only check the first initialView+dy rows for characters.
// * If we increase the height, rows will be added at the bottom. We'll
// want to check the initial viewport height for the original
// characters, but then we'll want to look for more blank rows at the
// bottom. The characters in the initial viewport won't have moved.

const short originalViewHeight = ::base::saturated_cast<short>(resizeDy < 0 ?
initialHostView.Height() + resizeDy :
initialHostView.Height());
const auto rowsWithText = std::min(originalViewHeight - 1, printedRows);
const bool scrolled = printedRows > initialHostView.Height();
// The last row of the viewport should be empty
// The second last row will have '0'+50
// The third last row will have '0'+49
// ...
// The <height> last row will have '0'+(50-height+1)
const auto firstChar = static_cast<wchar_t>(scrolled ?
(printedRows - originalViewHeight + 1) :
0);

short row = 0;
// Don't include the last row of the viewport in this check, since it'll
// be blank. We'll check it in the below loop.
for (; row < rowsWithText; row++)
{
SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures);
auto iter = hostTb.GetCellDataAt({ 0, row });

const auto expectedChar = static_cast<wchar_t>(((firstChar + row) % 93) + 33);
auto expectedString = std::wstring(1, static_cast<wchar_t>(expectedChar));

if (iter->Chars() != expectedString)
{
Log::Comment(NoThrowString().Format(L"row [%d] was mismatched", row));
}
VERIFY_ARE_EQUAL(expectedString, (iter++)->Chars(), NoThrowString().Format(L"%s", expectedString.data()));
VERIFY_ARE_EQUAL(L" ", (iter)->Chars());
}

// Check that the remaining rows in the viewport are empty.
for (; row < hostView.Height(); row++)
{
SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures);
auto iter = hostTb.GetCellDataAt({ 0, row });
VERIFY_ARE_EQUAL(L" ", (iter)->Chars());
}
};

verifyHostData(*hostTb);
verifyTermData(*termTb);

const COORD newViewportSize{
::base::saturated_cast<short>(TerminalViewWidth + dx),
::base::saturated_cast<short>(TerminalViewHeight + dy)
};

Log::Comment(NoThrowString().Format(L"Resize the Terminal and conpty here"));
auto resizeResult = term->UserResize(newViewportSize);
VERIFY_SUCCEEDED(resizeResult);
_resizeConpty(newViewportSize.X, newViewportSize.Y);

// After we resize, make sure to get the new textBuffers
hostTb = &si.GetTextBuffer();
termTb = term->_buffer.get();

// Conpty's doesn't have a scrollback, it's view's origin is always 0,0
const auto thirdHostView = si.GetViewport();
VERIFY_ARE_EQUAL(0, thirdHostView.Top());
VERIFY_ARE_EQUAL(newViewportSize.Y, thirdHostView.BottomExclusive());

// The Terminal should be stuck to the top of the viewport, unless dy<0,
// rows=50. In that set of cases, we _didn't_ pin the top of the Terminal to
// the old top, we actually shifted it down (because the output was at the
// bottom of the window, not empty lines).
const auto thirdTermView = term->GetViewport();
if (dy < 0 && (printedRows > initialTermView.Height() && printedRows < initialTerminalBufferHeight))
{
VERIFY_ARE_EQUAL(secondTermView.Top() - dy, thirdTermView.Top());
VERIFY_ARE_EQUAL(expectedTerminalViewBottom, thirdTermView.BottomExclusive());
}
else
{
VERIFY_ARE_EQUAL(secondTermView.Top(), thirdTermView.Top());
VERIFY_ARE_EQUAL(expectedTerminalViewBottom + dy, thirdTermView.BottomExclusive());
}

verifyHostData(*hostTb, dy);
// Note that at this point, nothing should have changed with the Terminal.
verifyTermData(*termTb, dy);

Log::Comment(NoThrowString().Format(L"Paint a frame to update the Terminal"));
VERIFY_SUCCEEDED(renderer.PaintFrame());

// Conpty's doesn't have a scrollback, it's view's origin is always 0,0
const auto fourthHostView = si.GetViewport();
VERIFY_ARE_EQUAL(0, fourthHostView.Top());
VERIFY_ARE_EQUAL(newViewportSize.Y, fourthHostView.BottomExclusive());

// The Terminal should be stuck to the top of the viewport, unless dy<0,
// rows=50. In that set of cases, we _didn't_ pin the top of the Terminal to
// the old top, we actually shifted it down (because the output was at the
// bottom of the window, not empty lines).
const auto fourthTermView = term->GetViewport();
if (dy < 0 && (printedRows > initialTermView.Height() && printedRows < initialTerminalBufferHeight))
{
VERIFY_ARE_EQUAL(secondTermView.Top() - dy, thirdTermView.Top());
VERIFY_ARE_EQUAL(expectedTerminalViewBottom, thirdTermView.BottomExclusive());
}
else
{
VERIFY_ARE_EQUAL(secondTermView.Top(), thirdTermView.Top());
VERIFY_ARE_EQUAL(expectedTerminalViewBottom + dy, thirdTermView.BottomExclusive());
}
verifyHostData(*hostTb, dy);
verifyTermData(*termTb, dy);
}

void ConptyRoundtripTests::PassthroughClearScrollback()
{
Log::Comment(NoThrowString().Format(
Expand All @@ -684,6 +912,7 @@ void ConptyRoundtripTests::PassthroughClearScrollback()
auto& gci = g.getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer();
auto& hostSm = si.GetStateMachine();

auto& termTb = *term->_buffer;

_flushFirstFrame();
Expand Down
Loading